Alpha — v0.1.0. This module is in early testing. The API may change before a stable release. Feedback and bug reports are welcome.
A modern, fluent HTTP client for ProcessWire that makes consuming APIs effortless. Send HTTP requests, handle responses, upload files, manage authentication, execute concurrent requests, and download remote files - all with an elegant, chainable interface that feels natural in ProcessWire.
Features
- Full HTTP Methods: GET, POST, PUT, PATCH, DELETE, HEAD with fluent API
- Request Pooling: Execute multiple requests concurrently for better performance
- Authentication: Built-in support for Bearer tokens and Basic Auth
- File Operations: Upload files with multipart/form-data and download remote files
- Smart Retry: Automatic retry logic with configurable attempts
- Flexible Timeouts: Separate connection and request timeout settings
- URL Templating: Dynamic URL parameters with {placeholder} syntax
- Rich Responses: JSON parsing with dot notation, status helpers, header access
- Error Handling: Comprehensive exception system with context and response data
- Content Negotiation: Accept header helpers for JSON, HTML, and custom types
- Security: URL scheme validation and sanitization built-in
- Status Reference: Complete HTTP status code dictionary included
Installation
Install as a standalone ProcessWire module:
- Copy the
SimpleClientfolder to/site/modules/ - Go to Modules → Refresh in the ProcessWire admin
- Install SimpleClient
Quick Access
// Global function (recommended)
client('https://api.example.com')->get('/users');
// Module instance
wire()->simpleclient->newClient('https://api.example.com');
// Via module function
simpleclient()->newClient('https://api.example.com');Quick Start
After installation, use the global client() function to make HTTP requests:
Basic Usage
<?php
// Simple GET request
$response = client('https://api.github.com')->get('/users/octocat');
echo $response->body();
// POST with JSON data
$response = client('https://api.example.com')
->withBody(json_encode(['name' => 'John', 'email' => 'john@example.com']))
->post('/users');
// Check response status
if ($response->successful()) {
$data = $response->json();
echo $data['id'];
}
// With authentication
$response = client('https://api.example.com')
->withToken('your-api-token')
->get('/protected-resource');
// Download a file
client()->download(
'https://example.com/file.pdf',
$config->paths->files . 'downloads/file.pdf'
);With SimpleRouter Integration
<?php
// /site/templates/api-proxy.php
namespace ProcessWire;
$page->route("get:github/user/{username}", function($username) {
try {
$response = client('https://api.github.com')
->get("/users/{$username}");
if ($response->notFound()) {
return response()->json(['error' => 'User not found'], 404);
}
return response()->json($response->json());
} catch (ClientException $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
});
$result = $page->dispatchRoutes();
if ($result !== null) {
echo $result;
}Making Requests
HTTP Methods
// GET request
$response = client('https://api.example.com')->get('/users');
// POST request
$response = client('https://api.example.com')
->post('/users', ['name' => 'John', 'email' => 'john@example.com']);
// PUT request
$response = client('https://api.example.com')
->put('/users/123', ['name' => 'John Updated']);
// PATCH request
$response = client('https://api.example.com')
->patch('/users/123', ['email' => 'newemail@example.com']);
// DELETE request
$response = client('https://api.example.com')
->delete('/users/123');
// HEAD request (only checks if resource exists)
$response = client('https://api.example.com')->head('/users/123');Base URL
// Set base URL to avoid repetition
$client = client('https://api.github.com');
$users = $client->get('/users');
$repos = $client->get('/repositories');
$orgs = $client->get('/organizations');
// Each request uses the base URL automaticallyRequest Body
// JSON body
$response = client('https://api.example.com')
->withBody(json_encode(['name' => 'Product', 'price' => 99.99]))
->post('/products');
// Form data
$response = client('https://api.example.com')
->asForm()
->post('/contact', [
'name' => 'John Doe',
'email' => 'john@example.com',
'message' => 'Hello!'
]);
// Can also use withBody() directly
$response = client('https://api.example.com')
->withBody(['name' => 'John'])
->post('/users');Headers
Setting Headers
// Add single header
$response = client('https://api.example.com')
->withHeaders(['X-Custom-Header' => 'value'])
->get('/data');
// Add multiple headers
$response = client('https://api.example.com')
->withHeaders([
'X-API-Key' => 'your-key',
'X-API-Version' => '2.0',
'User-Agent' => 'MyApp/1.0'
])
->get('/data');
// Replace all headers
$response = client('https://api.example.com')
->replaceHeaders(['Content-Type' => 'application/json'])
->post('/data');
// Accept header helpers
$response = client('https://api.example.com')
->acceptJson()
->get('/data');
$response = client('https://api.example.com')
->acceptHtml()
->get('/page');
// Custom accept type
$response = client('https://api.example.com')
->accept('application/xml')
->get('/data');Authentication
Bearer Token
// Bearer token authentication
$response = client('https://api.example.com')
->withToken('your-access-token')
->get('/protected-resource');
// Automatically adds: Authorization: Bearer your-access-tokenBasic Authentication
// Basic auth
$response = client('https://api.example.com')
->withBasicAuth('username', 'password')
->get('/protected');
// Automatically adds: Authorization: Basic base64(username:password)URL Parameters
Query Parameters
// Add query parameters
$response = client('https://api.example.com')
->withQueryParameters(['page' => 2, 'limit' => 50])
->get('/users');
// Results in: /users?page=2&limit=50
// Merge with an existing query string already in the URL
$response = client('https://api.example.com')
->withQueryParameters(['sort' => 'name', 'order' => 'asc'])
->get('/users?active=true');
// Results in: /users?active=true&sort=name&order=ascURL Template Parameters
// Dynamic URL segments with placeholders
$response = client('https://api.example.com')
->withUrlParameters(['id' => 123, 'action' => 'activate'])
->get('/users/{id}/{action}');
// Results in: /users/123/activate
// Useful for RESTful APIs
$userId = 456;
$response = client('https://api.example.com')
->withUrlParameters(['userId' => $userId])
->get('/users/{userId}/posts');File Operations
File Uploads
// Upload single file
$response = client('https://api.example.com')
->attach('avatar', '/path/to/avatar.jpg', 'image/jpeg', 'avatar.jpg')
->post('/users/123/avatar');
// Upload multiple files
$response = client('https://api.example.com')
->attach('document', '/path/to/doc.pdf', 'application/pdf', 'document.pdf')
->attach('image', '/path/to/img.png', 'image/png', 'image.png')
->post('/upload');
// Upload file with additional form data
$response = client('https://api.example.com')
->attach('file', '/path/to/file.zip', 'application/zip', 'archive.zip')
->withBody(['description' => 'Backup archive', 'category' => 'backups'])
->post('/files/upload');File Downloads
// Download file
$success = client()->download(
'https://example.com/large-file.zip',
$config->paths->files . 'downloads/file.zip'
);
if ($success) {
echo "File downloaded successfully!";
} else {
echo "Download failed: " . client()->getError();
}
// Download with authentication
$success = client()
->withToken('api-token')
->download(
'https://api.example.com/secure-file.pdf',
'/path/to/save/file.pdf'
);
// Directory is created automatically if it doesn't exist
$success = client()->download(
'https://example.com/report.pdf',
$config->paths->files . 'reports/2024/january/report.pdf'
);Timeouts & Retry
Setting Timeouts
// Request timeout (total time for request)
$response = client('https://slow-api.com')
->timeout(60) // 60 seconds
->get('/large-dataset');
// Connection timeout (time to establish connection)
$response = client('https://api.example.com')
->connectTimeout(5) // 5 seconds
->get('/data');
// Both timeouts
$response = client('https://api.example.com')
->timeout(30)
->connectTimeout(10)
->get('/data');Retry Logic
// Retry failed requests
$response = client('https://unreliable-api.com')
->retry(3) // Retry up to 3 times
->get('/data');
// Combine with timeout
$response = client('https://api.example.com')
->timeout(10)
->retry(2)
->get('/data');
// If request fails, it will retry automatically
// Total attempts = retry_attempts + 1 initial attemptResponse Handling
Response Body
// Get raw body
$response = client('https://api.example.com')->get('/data');
$body = $response->body();
// Parse JSON
$data = $response->json();
// Get specific JSON key
$name = $response->json('name');
// Get nested value with dot notation
$city = $response->json('user.address.city');
// With default value
$country = $response->json('user.address.country', 'USA');
// Parse as object
$obj = $response->object();
echo $obj->name;
// Get as collection (array)
$items = $response->collect();
$users = $response->collect('users'); // Get specific key as arrayStatus Checking
// Get status code
$status = $response->status(); // 200, 404, etc.
// Status categories
if ($response->successful()) { // 2xx
echo "Success!";
}
if ($response->redirect()) { // 3xx
echo "Redirected";
}
if ($response->failed()) { // 4xx or 5xx
echo "Request failed";
}
if ($response->clientError()) { // 4xx
echo "Client error";
}
if ($response->serverError()) { // 5xx
echo "Server error";
}Specific Status Codes
// Convenience methods for common status codes
if ($response->ok()) { // 200
echo "OK!";
}
if ($response->created()) { // 201
echo "Resource created";
}
if ($response->accepted()) { // 202
echo "Accepted for processing";
}
if ($response->noContent()) { // 204
echo "No content";
}
if ($response->notFound()) { // 404
echo "Not found";
}
if ($response->unauthorized()) { // 401
echo "Authentication required";
}
if ($response->forbidden()) { // 403
echo "Access denied";
}
if ($response->unprocessableEntity()) { // 422
echo "Validation failed";
}
if ($response->tooManyRequests()) { // 429
echo "Rate limit exceeded";
}
if ($response->internalServerError()) { // 500
echo "Server error";
}
if ($response->serviceUnavailable()) { // 503
echo "Service unavailable";
}
// See ClientResponse for complete listResponse Headers
// Get specific header (case-insensitive)
$contentType = $response->header('Content-Type');
$rateLimit = $response->header('X-RateLimit-Remaining');
// Get all headers
$headers = $response->headers();
// Convenience methods
$contentType = $response->getContentType();
// Check if response is JSON
if ($response->isJson()) {
$data = $response->json();
}
// Get cookies from response
$cookies = $response->getCookies();
// Returns: ['session_id' => 'abc123', 'user_pref' => 'dark']Request Pooling (Concurrent Requests)
Basic Pool Usage
// Execute multiple requests concurrently
$responses = client('https://api.example.com')
->pool(function($pool) {
$pool->get('/users');
$pool->get('/posts');
$pool->get('/comments');
return $pool->getHandles();
});
// Responses are keyed by request order
$users = $responses[0]->json();
$posts = $responses[1]->json();
$comments = $responses[2]->json();Named Pool Requests
// Name your requests for easier access
$responses = client('https://api.example.com')
->pool(function($pool) {
$pool->as('users')->get('/users');
$pool->as('posts')->get('/posts');
$pool->as('comments')->get('/comments');
return $pool->getHandles();
});
// Access by name
$users = $responses['users']->json();
$posts = $responses['posts']->json();
$comments = $responses['comments']->json();Pool with Options
// Pool with shared options
$responses = client('https://api.example.com')
->withToken('api-token')
->pool(function($pool) {
// All requests inherit the token
$pool->withQueryParameters(['limit' => 10])
->as('users')
->get('/users');
$pool->withQueryParameters(['published' => true])
->as('posts')
->get('/posts');
$pool->as('stats')
->get('/stats');
return $pool->getHandles();
});
// Different methods in pool
$responses = client('https://api.example.com')
->withToken('token')
->pool(function($pool) {
$pool->as('list')->get('/items');
$pool->as('create')->post('/items', ['name' => 'New Item']);
$pool->as('update')->put('/items/123', ['name' => 'Updated']);
$pool->as('delete')->delete('/items/456');
return $pool->getHandles();
});Pool Performance Benefits
// Sequential (slow) - 3 seconds if each takes 1 second
$users = client('https://api.example.com')->get('/users');
$posts = client('https://api.example.com')->get('/posts');
$comments = client('https://api.example.com')->get('/comments');
// Total time: ~3 seconds
// Concurrent (fast) - 1 second total
$responses = client('https://api.example.com')
->pool(function($pool) {
$pool->as('users')->get('/users');
$pool->as('posts')->get('/posts');
$pool->as('comments')->get('/comments');
return $pool->getHandles();
});
// Total time: ~1 second (all execute in parallel)Status Utilities
Quick Status Check
// Get just the status code
$status = client()->status('https://example.com');
echo $status; // 200, 404, 0 if failed
// Get status with text
$statusText = client()->statusText('https://example.com');
echo $statusText; // "200 OK", "404 Not Found", "0 Error"
// Check if URL is accessible
if (client()->status('https://example.com/page') === 200) {
echo "Page is accessible";
}
// Handle errors
$status = client()->status('https://invalid-domain.example');
if ($status === 0) {
echo "Error: " . client()->getError();
}HTTP Status Code Reference
// Get all HTTP status codes
$codes = client()->getHttpCodes();
// Returns: [100 => 'Continue', 200 => 'OK', 404 => 'Not Found', ...]
// Useful for documentation or status pages
foreach (client()->getHttpCodes() as $code => $description) {
echo "{$code}: {$description}";
}Error Handling
Namespace note:
ClientExceptionlives in theSimpleWire\Clientnamespace. In ProcessWire templates (which run in theProcessWirenamespace) use the fully-qualified class name\SimpleWire\Client\ClientException, or adduse SimpleWire\Client\ClientException;at the top of your file.
Exception Handling
use SimpleWire\Client\ClientException;
try {
$response = client('https://api.example.com')
->get('/data');
echo $response->json('message');
} catch (ClientException $e) {
// Get error message
echo "Error: " . $e->getMessage();
// Get error code
echo "Code: " . $e->getCode();
// Check if response is available
if ($e->hasResponse()) {
$response = $e->getResponse();
echo "Status: " . $response->status();
echo "Body: " . $response->body();
}
}Error Types
try {
$response = client('https://api.example.com')->get('/data');
} catch (ClientException $e) {
// Get error context
$context = $e->getContext();
$errorType = $context['type'] ?? 'unknown';
// $context['type'] is either 'curl_error' or 'http_error'
match ($errorType) {
'curl_error' => handleCurlError($e),
'http_error' => handleHttpError($e),
default => handleUnknownError($e)
};
}
function handleHttpError($e) {
$status = $e->getResponseStatus();
$body = $e->getResponseBody();
if ($status === 404) {
echo "Resource not found";
} elseif ($status === 401) {
echo "Authentication required";
} else {
echo "HTTP Error {$status}: {$body}";
}
}Getting Last Error
// For methods that return bool (download, status)
$success = client()->download('https://example.com/file.zip', '/tmp/file.zip');
if (!$success) {
$error = client()->getError();
echo "Download failed: {$error}";
}
// Clear error
client()->clearError();
// Check status
$status = client()->status('https://example.com');
if ($status === 0) {
echo client()->getError();
}Security
URL Scheme Validation
// By default, only http and https are allowed
$response = client()->get('https://example.com'); // ✓ OK
$response = client()->get('http://example.com'); // ✓ OK
$response = client()->get('ftp://example.com'); // ✗ Exception
// Configure allowed schemes
client()
->setAllowSchemes(['http', 'https', 'ftp'])
->get('ftp://example.com'); // ✓ Now OK
// Get current allowed schemes
$schemes = client()->getAllowSchemes();
// Returns: ['http', 'https']URL Validation
// URLs are validated using ProcessWire's sanitizer
// Configure validation options
client()
->setValidateURLOptions([
'allowRelative' => false,
'allowIDN' => true
])
->get('/api/data');
// Invalid URLs throw exceptions automaticallyComplete Examples
GitHub API Integration
<?php
// Fetch user repositories from GitHub
function getGithubRepos(string $username): array
{
try {
$response = client('https://api.github.com')
->withHeaders(['User-Agent' => 'ProcessWire-App'])
->get("/users/{$username}/repos");
if ($response->notFound()) {
return ['error' => 'User not found'];
}
$repos = $response->collect();
return array_map(function($repo) {
return [
'name' => $repo['name'],
'description' => $repo['description'] ?? '',
'stars' => $repo['stargazers_count'],
'url' => $repo['html_url']
];
}, $repos);
} catch (ClientException $e) {
return ['error' => $e->getMessage()];
}
}
// Usage
$repos = getGithubRepos('torvalds');
foreach ($repos as $repo) {
echo "{$repo['name']}: {$repo['stars']} stars\n";
}
?>REST API CRUD Operations
<?php
class ProductAPI
{
private \SimpleWire\Client\Client $client;
public function __construct(string $apiKey)
{
$this->client = client('https://api.example.com')
->withToken($apiKey)
->acceptJson()
->timeout(30);
}
public function listProducts(int $page = 1): array
{
$response = $this->client
->withQueryParameters(['page' => $page, 'limit' => 20])
->get('/products');
return $response->json('data', []);
}
public function getProduct(int $id): ?array
{
try {
$response = $this->client->get("/products/{$id}");
return $response->json();
} catch (ClientException $e) {
if ($e->getResponseStatus() === 404) {
return null;
}
throw $e;
}
}
public function createProduct(array $data): array
{
$response = $this->client
->withBody(json_encode($data))
->post('/products');
if ($response->created()) {
return $response->json();
}
throw new \Exception('Failed to create product');
}
public function updateProduct(int $id, array $data): array
{
$response = $this->client
->withBody(json_encode($data))
->put("/products/{$id}");
return $response->json();
}
public function deleteProduct(int $id): bool
{
$response = $this->client->delete("/products/{$id}");
return $response->successful();
}
}
// Usage
$api = new ProductAPI('your-api-key');
// List products
$products = $api->listProducts(1);
// Get single product
$product = $api->getProduct(123);
// Create product
$newProduct = $api->createProduct([
'name' => 'New Product',
'price' => 99.99,
'category' => 'Electronics'
]);
// Update product
$updated = $api->updateProduct(123, ['price' => 89.99]);
// Delete product
$api->deleteProduct(123);
?>Parallel Data Fetching
<?php
// Fetch data from multiple sources concurrently
function getDashboardData(): array
{
$responses = client('https://api.example.com')
->withToken('api-token')
->pool(function($pool) {
$pool->as('users')->get('/stats/users');
$pool->as('revenue')->get('/stats/revenue');
$pool->as('orders')->get('/stats/orders');
$pool->as('traffic')->get('/stats/traffic');
return $pool->getHandles();
});
return [
'users' => $responses['users']->json('total', 0),
'revenue' => $responses['revenue']->json('total', 0),
'orders' => $responses['orders']->json('count', 0),
'traffic' => $responses['traffic']->json('visits', 0)
];
}
// This executes all 4 requests concurrently
// If each request takes 500ms, total time is ~500ms instead of ~2000ms
$data = getDashboardData();
echo "Users: {$data['users']}\n";
echo "Revenue: {$data['revenue']}\n";
?>File Upload with Progress
<?php
$page->route("post:upload/document", function() {
// Validate file
if (!request()->hasFile('document')) {
return response()->json(['error' => 'No file uploaded'], 400);
}
$file = request()->file('document');
$tempPath = $file['tmp_name'];
try {
// Upload to external storage API
$response = client('https://storage.example.com')
->withToken('storage-api-key')
->attach('file', $tempPath, $file['type'], $file['name'])
->withBody([
'folder' => 'documents',
'public' => false
])
->post('/upload');
if ($response->created()) {
$data = $response->json();
return response()->json([
'success' => true,
'file_id' => $data['id'],
'url' => $data['url']
]);
}
return response()->json(['error' => 'Upload failed'], 500);
} catch (ClientException $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
});
?>Webhook Receiver with Verification
<?php
$page->route("post:webhooks/stripe", function() {
// Verify webhook signature
$signature = request()->header('Stripe-Signature');
$payload = request()->body();
if (!verifyStripeSignature($signature, $payload)) {
return response()->json(['error' => 'Invalid signature'], 401);
}
$event = request()->json();
// Process event
match ($event['type']) {
'payment_intent.succeeded' => handlePaymentSuccess($event['data']),
'payment_intent.failed' => handlePaymentFailure($event['data']),
'customer.created' => handleCustomerCreated($event['data']),
default => null
};
// Acknowledge receipt
return response()->json(['received' => true]);
});
function handlePaymentSuccess($data): void
{
$paymentId = $data['object']['id'];
$amount = $data['object']['amount'] / 100;
// Notify internal system
try {
client('https://internal-api.example.com')
->withToken('internal-token')
->post('/payments/confirm', [
'payment_id' => $paymentId,
'amount' => $amount,
'status' => 'completed'
]);
} catch (ClientException $e) {
wire('log')->error("Failed to notify internal system: " . $e->getMessage());
}
}
?>Data Synchronization
<?php
// Sync products from external API to ProcessWire
function syncProducts(): array
{
$results = ['success' => 0, 'failed' => 0, 'errors' => []];
try {
// Fetch products from API
$response = client('https://supplier-api.example.com')
->withToken('supplier-key')
->withQueryParameters(['updated_since' => date('Y-m-d', strtotime('-1 day'))])
->get('/products');
if (!$response->successful()) {
$results['errors'][] = 'Failed to fetch products';
return $results;
}
$products = $response->json('products', []);
foreach ($products as $productData) {
try {
// Check if product exists
$product = wire('pages')->get("template=product, sku={$productData['sku']}");
if (!$product->id) {
// Create new product
$product = new Page();
$product->template = 'product';
$product->parent = wire('pages')->get('/products/');
}
// Update product data
$product->title = $productData['name'];
$product->sku = $productData['sku'];
$product->price = $productData['price'];
$product->description = $productData['description'];
$product->stock = $productData['stock'];
$product->save();
// Download product image if URL provided
if (!empty($productData['image_url'])) {
$imagePath = wire('config')->paths->files . $product->id . '/';
$imageFile = $imagePath . basename($productData['image_url']);
$downloaded = client()->download($productData['image_url'], $imageFile);
if ($downloaded) {
$product->images->add($imageFile);
$product->save();
}
}
$results['success']++;
} catch (\Exception $e) {
$results['failed']++;
$results['errors'][] = "Product {$productData['sku']}: " . $e->getMessage();
}
}
} catch (ClientException $e) {
$results['errors'][] = 'API Error: ' . $e->getMessage();
}
return $results;
}
// Run sync
$results = syncProducts();
echo "Synced {$results['success']} products, {$results['failed']} failed\n";
if (!empty($results['errors'])) {
foreach ($results['errors'] as $error) {
echo "Error: {$error}\n";
}
}
?>Rate-Limited API Client
<?php
class RateLimitedAPIClient
{
private \SimpleWire\Client\Client $client;
private string $cacheKey = 'api_rate_limit';
private int $maxRequests = 100;
private int $perSeconds = 60;
public function __construct(string $apiKey)
{
$this->client = client('https://api.example.com')
->withToken($apiKey)
->acceptJson();
}
private function checkRateLimit(): bool
{
$requests = wire('cache')->get($this->cacheKey) ?: 0;
if ($requests >= $this->maxRequests) {
return false;
}
wire('cache')->save($this->cacheKey, $requests + 1, $this->perSeconds);
return true;
}
public function request(string $method, string $url, array $data = []): ClientResponse
{
if (!$this->checkRateLimit()) {
throw new \Exception('Rate limit exceeded. Please try again later.');
}
return match(strtoupper($method)) {
'GET' => $this->client->get($url),
'POST' => $this->client->post($url, $data),
'PUT' => $this->client->put($url, $data),
'DELETE' => $this->client->delete($url),
default => throw new \Exception('Unsupported method')
};
}
public function getRemainingRequests(): int
{
$requests = wire('cache')->get($this->cacheKey) ?: 0;
return max(0, $this->maxRequests - $requests);
}
}
// Usage
$api = new RateLimitedAPIClient('api-key');
if ($api->getRemainingRequests() > 0) {
$response = $api->request('GET', '/data');
echo $response->json('message');
} else {
echo "Rate limit exceeded";
}
?>Multi-Source Data Aggregation
<?php
// Aggregate data from multiple APIs
function getWeatherData(string $city): array
{
// Fetch from multiple weather APIs concurrently
$responses = client()
->pool(function($pool) use ($city) {
$pool->as('openweather')
->withQueryParameters(['q' => $city, 'appid' => 'key1'])
->get('https://api.openweathermap.org/data/2.5/weather');
$pool->as('weatherapi')
->withQueryParameters(['key' => 'key2', 'q' => $city])
->get('https://api.weatherapi.com/v1/current.json');
return $pool->getHandles();
});
// Combine results
$aggregated = [
'city' => $city,
'sources' => [],
'average_temp' => 0
];
$temps = [];
// OpenWeather data
if ($responses['openweather']->successful()) {
$data = $responses['openweather']->json();
$temp = $data['main']['temp'] - 273.15; // Convert Kelvin to Celsius
$temps[] = $temp;
$aggregated['sources'][] = [
'name' => 'OpenWeather',
'temp' => round($temp, 1),
'conditions' => $data['weather'][0]['description']
];
}
// WeatherAPI data
if ($responses['weatherapi']->successful()) {
$data = $responses['weatherapi']->json();
$temp = $data['current']['temp_c'];
$temps[] = $temp;
$aggregated['sources'][] = [
'name' => 'WeatherAPI',
'temp' => round($temp, 1),
'conditions' => $data['current']['condition']['text']
];
}
// Calculate average
if (!empty($temps)) {
$aggregated['average_temp'] = round(array_sum($temps) / count($temps), 1);
}
return $aggregated;
}
// Usage
$weather = getWeatherData('Miami');
echo "Temperature in {$weather['city']}: {$weather['average_temp']}°C\n";
foreach ($weather['sources'] as $source) {
echo "{$source['name']}: {$source['temp']}°C - {$source['conditions']}\n";
}
?>Module Configuration
Configuration Options
Configured in the SimpleClient module settings.
client_timeout— Maximum time for request completion (default: 30 seconds)client_connectTimeout— Maximum time to establish connection (default: 10 seconds)client_retryAttempts— Number of automatic retries on failure (default: 0)client_verifySsl— Enable SSL certificate verification (default: true)client_followRedirects— Automatically follow HTTP redirects (default: true)client_maxRedirects— Maximum redirects to follow (default: 5)client_userAgent— User agent string sent with requests (default:SimpleWire/Client 1.0)client_allowedSchemes— Comma-separated list of URL schemes allowed in requests (default:http, https)
Setting Defaults
Configure defaults in the module settings, or override per-request:
// Use module defaults
$response = client('https://api.example.com')->get('/data');
// Override for specific request
$response = client('https://api.example.com')
->timeout(60)
->connectTimeout(15)
->retry(3)
->get('/data');Best Practices
- Use Base URLs: Set base URL once to avoid repetition
- Handle Exceptions: Always wrap requests in try-catch blocks
- Check Response Status: Use
successful(),failed()before processing - Use Request Pooling: Execute concurrent requests for better performance
- Set Appropriate Timeouts: Configure timeouts based on expected response times
- Validate URLs: Use scheme validation for security
- Use Typed Methods: Prefer
json()over manual parsing - Log Errors: Log failed requests for debugging
- Cache Responses: Cache API responses when appropriate to reduce calls
- Use Retry Wisely: Enable retry for unreliable endpoints, disable for critical operations
Performance Tips
- Use Request Pooling: Dramatically reduce total request time with concurrent execution
- Set Realistic Timeouts: Don't wait longer than necessary
- Cache API Responses: Use ProcessWire's WireCache to cache responses
- Reuse Client Instances: Create client once with base URL and reuse
- Disable SSL Verification (Development Only): Only for local development
- Monitor Rate Limits: Respect API rate limits to avoid throttling
Security Considerations
Important Security Notes:
- Validate URLs: Always validate and sanitize user-provided URLs
- Use HTTPS: Prefer HTTPS for sensitive data transmission
- Store Tokens Securely: Never hardcode API tokens, use environment variables or config
- Verify Webhooks: Always verify webhook signatures before processing
- Sanitize Input: Sanitize data before sending to external APIs
- Handle Errors Gracefully: Don't expose sensitive error details to users
- Rate Limiting: Implement rate limiting for outbound requests
- Timeout Protection: Always set timeouts to prevent hanging requests
- SSL Verification: Keep SSL verification enabled in production
Troubleshooting
Request timing out:
- Increase timeout with
timeout()method - Check if remote server is responding slowly
- Verify network connectivity
- Check if firewall is blocking outbound requests
SSL certificate errors:
- Update CA certificates on your server
- Verify the remote server's SSL certificate is valid
- For development only: disable SSL verification via
client()->setValidateURLOptions([])or the module config - In production: SSL verification is enabled by default — keep it that way
File upload failing:
- Check file permissions on the source file
- Verify file size doesn't exceed server limits
- Ensure correct MIME type is specified
- Check remote API's file upload requirements
Response parsing errors:
- Verify response is valid JSON with
isJson() - Check response status before parsing
- Use
json()with default values to handle missing keys - Log raw response body for debugging
Authentication failing:
- Verify token/credentials are correct
- Check if token has expired
- Ensure proper Authorization header format
- Test authentication separately before complex requests
Debugging
// Log request details
try {
$response = client('https://api.example.com')
->withHeaders(['X-Debug' => 'true'])
->get('/data');
// Log response
wire('log')->save('api-calls', sprintf(
'URL: %s, Status: %d, Body: %s',
'https://api.example.com/data',
$response->status(),
$response->body()
));
} catch (ClientException $e) {
// Log error with full context
wire('log')->error(sprintf(
'API Error: %s, Code: %d, Context: %s',
$e->getMessage(),
$e->getCode(),
json_encode($e->getContext())
));
// If response available, log it
if ($e->hasResponse()) {
$response = $e->getResponse();
wire('log')->error(sprintf(
'Response Status: %d, Body: %s',
$response->status(),
$response->body()
));
}
}
// Debug download issues
$success = client()->download('https://example.com/file.zip', '/tmp/file.zip');
if (!$success) {
wire('log')->error('Download failed: ' . client()->getError());
}
// Test URL accessibility
$status = client()->status('https://api.example.com/health');
wire('log')->message("API Health Check: " . client()->statusText('https://api.example.com/health'));API Reference
Global Function
client(string $baseUrl = '', array $config = []): \SimpleWire\Client\ClientRequest Methods
get(string $url, array $options = []): ClientResponsepost(string $url, $body = null, array $options = []): ClientResponseput(string $url, $body = null, array $options = []): ClientResponsepatch(string $url, $body = null, array $options = []): ClientResponsedelete(string $url, array $options = []): ClientResponsestatus(string $url): intstatusText(string $url): stringdownload(string $fromURL, string $toFile): bool
Request Configuration
withBody($body): selfasForm(): selfattach(string $name, string $path, string $mime, string $filename): selfwithHeaders(array $headers): selfreplaceHeaders(array $headers): selfaccept(string $type): selfacceptJson(): selfacceptHtml(): selfwithQueryParameters(array $params): selfwithUrlParameters(array $params): selftimeout(int $seconds): selfconnectTimeout(int $seconds): selfretry(int $attempts): self
Authentication
withToken(string $token): selfwithBasicAuth(string $username, string $password): self
Request Pooling
pool(callable $callback): array
Utilities
getHttpCodes(): arraygetError(): ?stringclearError(): selfsetAllowSchemes(array $schemes): selfgetAllowSchemes(): arraysetValidateURLOptions(array $options): self
Response Methods
body(): stringjson($key = null, $default = null): mixedobject(): objectcollect($key = null): arraystatus(): intheader(string $header): stringheaders(): arraysuccessful(): bool- 2xxredirect(): bool- 3xxfailed(): bool- 4xx or 5xxclientError(): bool- 4xxserverError(): bool- 5xxok(): bool- 200created(): bool- 201accepted(): bool- 202noContent(): bool- 204notFound(): bool- 404unauthorized(): bool- 401forbidden(): bool- 403unprocessableEntity(): bool- 422tooManyRequests(): bool- 429internalServerError(): bool- 500serviceUnavailable(): bool- 503isJson(): boolgetContentType(): stringgetCookies(): array
Exception Methods
getResponse(): ?ClientResponsehasResponse(): boolgetResponseBody(): stringgetResponseStatus(): intgetResponseHeaders(): arraygetContext(): array
Comparison with Other Clients
| Feature | SimpleWire Client | Guzzle | cURL |
|---|---|---|---|
| Installation | SimpleClient Module | Composer Required | PHP Extension |
| Fluent API | ✅ | ✅ | ❌ |
| Request Pooling | ✅ | ✅ | Manual |
| JSON Dot Notation | ✅ | ❌ | ❌ |
| ProcessWire Integration | ✅ Native | Manual | Manual |
| File Download Helper | ✅ | Manual | Manual |
| Learning Curve | Low | Medium | High |
License
This module is released under the MIT License.
More modules by WireCodex
- Added 1 week ago by WireCodex
- Added 3 days ago by WireCodex
- Added 3 days ago by WireCodex
- Added 3 days ago by WireCodex
SimpleQuery
GraphQL-like query engine for ProcessWire pages with caching, rate limiting, and write support.0Added 3 days ago by WireCodexSimpleQueue
Background job queue with priority, delayed execution, retry, and LazyCron-based processing.0Added 3 days ago by WireCodexSimpleAsset
Asset management for ProcessWire. Resolves, groups, and renders CSS/JS assets from CDN sources or local paths with cache-busting, SRI, and inline threshold support.0Added 3 days ago by WireCodex
Install and use modules at your own risk. Always have a site and database backup before installing new modules.