Encode and decode JSON, make HTTP requests with PHP streams and cURL, build a simple REST client, and parse API responses with error handling.
Background
JSON is the lingua franca of modern APIs. PHP's json_encode/json_decode functions are fast and flexible. Making HTTP requests in PHP uses either file_get_contents with a stream context (built-in), cURL (more control), or libraries like Guzzle. Understanding raw HTTP in PHP prepares you to use any framework's HTTP client.
Time
35 minutes
Prerequisites
Lab 09 (Error Handling), Lab 10 (File I/O)
Tools
PHP 8.3 CLI
Docker image: zchencow/innozverse-php:latest
Lab Instructions
Step 1: json_encode & json_decode
💡 JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE are the most useful flags. Without JSON_UNESCAPED_UNICODE, Chinese/emoji characters become \uXXXX escape sequences. Without JSON_UNESCAPED_SLASHES, / becomes \/. Both flags produce cleaner, human-readable JSON.
📸 Verified Output:
Step 2: JSON Schema Validation
📸 Verified Output:
Step 3: HTTP Request with file_get_contents
💡 $http_response_header is a PHP magic variable — after file_get_contents() with an HTTP URL, it's automatically populated with the response headers as an array of strings. ignore_errors: true prevents PHP warnings on 4xx/5xx responses so you can handle them yourself.
📸 Verified Output:
Step 4: HTTP POST with JSON Body
📸 Verified Output:
Step 5: Build a Simple REST Client
💡 Wrapping HTTP logic in a RestClient class gives you a clean API: $client->get('/users/1'). This is exactly what Guzzle does internally — it wraps cURL with a fluent interface. The key difference: Guzzle adds retry, middleware, async, and PSR-7 compliance.
📸 Verified Output:
Step 6: Parse & Transform API Responses
📸 Verified Output:
Step 7: Error Handling for HTTP
📸 Verified Output:
Step 8: Complete — Mock REST API Server
💡 Mock APIs let you test your REST client code without hitting real servers — no network dependency, deterministic responses, no rate limits. This pattern is how PHPUnit tests HTTP-dependent code. Laravel's Http::fake() and Guzzle's MockHandler work on the same principle.
📸 Verified Output:
Verification
Summary
JSON and HTTP are PHP's bread and butter. You've encoded/decoded JSON, made GET/POST requests with stream contexts, built a reusable REST client, handled API errors with custom exceptions, and implemented a mock API. These skills cover 90% of real-world PHP API integration work.
Post #1: sunt aut facere repellat provident occaecati
User: 1
Body preview: quia et suscipit suscipit recusandae consequuntur expedita...
Users:
Leanne Graham <[email protected]>
Ervin Howell <[email protected]>
Clementine Bauch <[email protected]>
Todos (HTTP 200):
[✓] delectus aut autem
[ ] quis ut nam facilis et officia qui
[ ] fugiat veniam minus
Created todo (HTTP 201): #201 Learn PHP APIs
<?php
$client = new RestClient('https://jsonplaceholder.typicode.com');
// Fetch and transform users
$usersResp = $client->get('/users');
$users = array_map(fn($u) => [
'id' => $u['id'],
'name' => $u['name'],
'email' => strtolower($u['email']),
'city' => $u['address']['city'],
'company' => $u['company']['name'],
], $usersResp['data']);
// Filter by city (contains 'South')
$southCities = array_filter($users, fn($u) => str_contains($u['city'], 'South'));
echo "Users in 'South' cities:\n";
foreach ($southCities as $u) {
echo " {$u['name']} — {$u['city']}\n";
}
// Group by first letter of name
$grouped = [];
foreach ($users as $u) {
$grouped[$u['name'][0]][] = $u['name'];
}
ksort($grouped);
echo "\nGrouped by initial:\n";
foreach ($grouped as $letter => $names) {
echo " $letter: " . implode(', ', $names) . "\n";
}
// Stats
$companies = array_count_values(array_column($users, 'company'));
arsort($companies);
echo "\nUsers per company:\n";
foreach (array_slice($companies, 0, 3, true) as $company => $count) {
echo " $company: $count\n";
}
Users in 'South' cities:
Kurtis Weissnat — South Elvis
Nicholas Runolfsdottir V — South Christy
Grouped by initial:
C: Chelsey Dietrich, Clementine Bauch
E: Ervin Howell
...
Users per company:
Romaguera-Crona: 1
...
<?php
declare(strict_types=1);
class ApiException extends \RuntimeException {
public function __construct(
public readonly int $statusCode,
public readonly string $endpoint,
string $message = '',
) {
parent::__construct($message ?: "API error $statusCode at $endpoint", $statusCode);
}
}
function apiGet(string $url): array {
$ctx = stream_context_create(['http' => ['ignore_errors' => true, 'timeout' => 10]]);
$response = @file_get_contents($url, false, $ctx);
if ($response === false) throw new \RuntimeException("Network error: cannot reach $url");
preg_match('/HTTP\/\S+ (\d+)/', $http_response_header[0] ?? '', $m);
$status = (int)($m[1] ?? 0);
if ($status === 404) throw new ApiException(404, $url, "Resource not found");
if ($status >= 500) throw new ApiException($status, $url, "Server error");
if ($status >= 400) throw new ApiException($status, $url, "Client error");
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE)
throw new \RuntimeException("Invalid JSON response from $url");
return $data;
}
$urls = [
'https://jsonplaceholder.typicode.com/posts/1',
'https://jsonplaceholder.typicode.com/posts/99999',
'https://jsonplaceholder.typicode.com/posts/1',
];
foreach ($urls as $url) {
try {
$data = apiGet($url);
echo "✓ " . basename($url) . ": " . ($data['title'] ?? $data['id']) . "\n";
} catch (ApiException $e) {
echo "✗ HTTP {$e->statusCode}: {$e->getMessage()}\n";
} catch (\RuntimeException $e) {
echo "✗ Error: {$e->getMessage()}\n";
}
}
✓ 1: sunt aut facere repellat provident occaecati
✗ HTTP 404: Resource not found
✓ 1: sunt aut facere repellat provident occaecati