24. PHP – JSON i REST API


JSON (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 (Representational State Transfer) to architektura tworzenia API wykorzystująca protokół HTTP do komunikacji.

1. JSON – Podstawy

Struktura JSON

{
  "name": "Jan Kowalski",
  "age": 25,
  "active": true,
  "email": null,
  "roles": ["admin", "editor"],
  "address": {
    "city": "Warszawa",
    "zip": "00-001"
  }
}

Typy danych w JSON:

  • String"tekst"
  • Number423.14
  • Booleantruefalse
  • nullnull
  • Array[1, 2, 3]
  • Object{"key": "value"}

2. json_encode() – Konwersja PHP → JSON

Podstawowe użycie

<?php
// Tablica asocjacyjna → JSON object
$user = [
    'id' => 123,
    'username' => 'ProGamer',
    'email' => 'pro@example.com',
    'active' => true,
    'balance' => 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
}
*/
?>

Opcje json_encode()

<?php
$data = [
    'name' => 'Janek',
    'city' => 'Kraków',
    'tags' => ['php', 'javascript'],
    'html' => '<script>alert("XSS")</script>',
    'unicode' => 'Zażółć gęślą jaźń',
    'url' => 'https://example.com/path?param=value&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&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 = ['age' => '25', 'price' => '19.99', 'name' => 'Jan'];
echo json_encode($mixed, JSON_NUMERIC_CHECK);
// {"age":25,"price":19.99,"name":"Jan"}

// JSON_FORCE_OBJECT - wymuś obiekt zamiast tablicy
$indexed_array = ['a', 'b', 'c'];
echo json_encode($indexed_array); // ["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);
?>

Sprawdzanie błędów json_encode()

<?php
$data = [
    'name' => 'Test',
    'invalid' => "\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() . "<br>";
    echo "Kod błędu: " . json_last_error() . "<br>";
    
    /*
    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}
?>

Przykład – Konwersja danych z bazy do JSON

<?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');

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

$users = [];
while ($row = $wynik->fetch_assoc()) {
    $users[] = $row;
}

$stmt->close();
$db->close();

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

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

3. json_decode() – Konwersja JSON → PHP

Podstawowe użycie

<?php
$json = '{"id":123,"username":"ProGamer","email":"pro@example.com","active":true}';

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

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

Głębokość dekodowania

<?php
$json = '{
    "user": {
        "profile": {
            "address": {
                "city": "Warszawa"
            }
        }
    }
}';

// Domyślna głębokość: 512
$data = json_decode($json, true);
echo $data['user']['profile']['address']['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();
}
?>

Sprawdzanie błędów json_decode()

<?php
// Nieprawidłowy JSON
$invalid_json = '{"name":"Test", "age":25,}'; // Przecinek na końcu!

$data = json_decode($invalid_json, true);

if ($data === null && json_last_error() !== JSON_ERROR_NONE) {
    echo "Błąd JSON: " . json_last_error_msg() . "<br>";
    echo "Kod błędu: " . json_last_error() . "<br>";
} 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->getMessage();
}
?>

Przykład – Odbieranie JSON od klienta

<?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([
        'success' => false,
        'error' => 'Invalid JSON: ' . json_last_error_msg()
    ]);
    exit;
}

// Przetwórz dane
$username = $data['username'] ?? '';
$email = $data['email'] ?? '';

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

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

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

4. REST API – Podstawy

REST (Representational State Transfer) to styl architektury API wykorzystujący:

  • Metody HTTP: GET, POST, PUT, DELETE
  • URL-e jako zasoby/users/users/123
  • Statusy HTTP: 200 OK, 404 Not Found, 500 Error
  • Format JSON do wymiany danych

Metody HTTP (CRUD)

MetodaOperacjaPrzykładOpis
GETReadGET /usersPobierz listę użytkowników
GETReadGET /users/123Pobierz użytkownika o ID 123
POSTCreatePOST /usersUtwórz nowego użytkownika
PUTUpdatePUT /users/123Zaktualizuj użytkownika 123 (cały zasób)
PATCHUpdatePATCH /users/123Zaktualizuj część danych użytkownika 123
DELETEDeleteDELETE /users/123Usuń użytkownika 123

Statusy HTTP – Najważniejsze

KodStatusZnaczenie
200OKSukces
201CreatedZasób utworzony
204No ContentSukces, brak treści do zwrócenia
400Bad RequestBłędne żądanie
401UnauthorizedBrak autoryzacji
403ForbiddenBrak uprawnień
404Not FoundNie znaleziono
500Internal Server ErrorBłąd serwera
KodStatusZnaczenie
200OKSukces
201CreatedZasób utworzony
204No ContentSukces, brak treści do zwrócenia
400Bad RequestBłędne żądanie
401UnauthorizedBrak autoryzacji
403ForbiddenBrak uprawnień
404Not FoundNie znaleziono
500Internal Server ErrorBłąd serwera
KodStatusZnaczenie
200OKSukces
201CreatedZasób utworzony
204No ContentSukces, brak treści do zwrócenia
400Bad RequestBłędne żądanie
401UnauthorizedBrak autoryzacji
403ForbiddenBrak uprawnień
404Not FoundNie znaleziono
500Internal Server ErrorBłąd serwera

5. Tworzenie prostego REST API

Struktura projektu

/api
  /v1
    index.php         # Router
    users.php         # Endpoint użytkowników
    config.php        # Konfiguracja
    Database.php      # Klasa bazy danych
    Response.php      # Klasa odpowiedzi

Plik: Response.php – Klasa pomocnicza

<?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 = [], $message = 'Success', $status_code = 200) {
        self::json([
            'success' => true,
            'message' => $message,
            'data' => $data
        ], $status_code);
    }
    
    public static function error($message = 'Error', $status_code = 400, $errors = []) {
        self::json([
            'success' => false,
            'message' => $message,
            'errors' => $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);
    }
}
?>

Plik: Database.php – Klasa bazy danych

<?php
// Database.php

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

Plik: users.php – API użytkowników

<?php
// users.php

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

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

// Wykonaj API
$api = new UsersAPI();
$api->handleRequest();
?>

Plik: index.php – Router

<?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['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit;
}

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

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

// Nieznany endpoint
require_once 'Response.php';
Response::notFound('Endpoint not found');
?>

Plik: .htaccess – URL Rewriting

# .htaccess

RewriteEngine On

# Przekieruj wszystko na index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]

6. Testowanie API

Przykłady żądań (curl)

# 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&limit=5"

# GET - Pobierz z filtrowaniem
curl -X GET "http://localhost/api/v1/users?level_min=10&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"

Testowanie w Postman

  1. Pobierz Postman z https://www.postman.com/
  2. Utwórz nową kolekcję „Gaming API”
  3. Dodaj żądania:
    • GET /users
      • Method: GET
      • URL: http://localhost/api/v1/users
      • Send
    • POST /users:
      • Method: POST
      • URL: http://localhost/api/v1/users
      • Headers: Content-Type: application/json
      • Body → raw → JSON:
{
  "username": "TestUser",
  "email": "test@example.com",
  "password": "password123"
}

7. Konsumowanie zewnętrznych API

file_get_contents() – Proste żądania GET

<?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 = [
    'http' => [
        'method' => 'GET',
        'header' => "User-Agent: MyApp/1.0\r\n" .
                    "Accept: application/json\r\n",
        'timeout' => 10
    ]
];

$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
$data = json_decode($response, true);
?>

cURL – Zaawansowane żądania

<?php
// Funkcja pomocnicza do żądań cURL
function apiRequest($url, $method = 'GET', $data = null, $headers = []) {
    
    $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[] = 'Content-Type: application/json';
        $headers[] = '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 [
        'status_code' => $status_code,
        'body' => json_decode($response, true)
    ];
}

// Przykłady użycia

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

// POST
$new_post = [
    'title' => 'Test Post',
    'body' => 'This is a test',
    'userId' => 1
];

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

echo "Created Post ID: " . $result['body']['id'];

// PUT
$updated_post = [
    'title' => 'Updated Title',
    'body' => 'Updated content',
    'userId' => 1
];

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

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

Przykład – Integracja z GitHub API

<?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['login']}<br>";
        echo "Name: {$user['name']}<br>";
        echo "Public Repos: {$user['public_repos']}<br>";
        echo "Followers: {$user['followers']}<br>";
        echo "<img src='{$user['avatar_url']}' width='100'><br>";
    } else {
        echo "User not found";
    }
    
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>

8. Autentykacja API

API Key – Prosty klucz dostępu

<?php
// Sprawdź API key w nagłówku
$headers = getallheaders();
$api_key = $headers['X-API-Key'] ?? '';

$valid_keys = [
    'abc123xyz' => 'user_1',
    'def456uvw' => 'user_2'
];

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

$user_id = $valid_keys[$api_key];
// Kontynuuj przetwarzanie...
?>

Bearer Token – Token w nagłówku Authorization

<?php
// Sprawdź Bearer token
$headers = getallheaders();
$auth_header = $headers['Authorization'] ?? '';

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

$token = $matches[1];

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

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

// Token OK - kontynuuj
$current_user_id = $user['id'];
?>

JWT (JSON Web Tokens) – Podstawy

<?php
// Prosty JWT (w produkcji użyj biblioteki firebase/php-jwt)

function generateJWT($user_id, $secret_key) {
    
    $header = [
        'typ' => 'JWT',
        'alg' => 'HS256'
    ];
    
    $payload = [
        'user_id' => $user_id,
        'iat' => time(),           // Issued at
        'exp' => 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['exp'] < 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<br>";

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

if ($payload) {
    echo "User ID: " . $payload['user_id'];
} else {
    echo "Invalid token";
}
?>

Przykład kompleksowy – API z autentykacją

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

$api = new AuthAPI();
$api->handleRequest();
?>