定制 callismart/http 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

callismart/http

最新稳定版本:v1.0.0

Composer 安装命令:

composer require callismart/http

包简介

A lightweight, multi-adapter HTTP client for PHP 8.1+ with AWS SigV4 support and fallbacks for restricted environments.

README 文档

README

A lightweight, multi-adapter HTTP client for PHP 8.1+ designed for maximum reliability across diverse hosting environments. It provides a clean, fluent API with native support for AWS Signature V4 and automatic adapter fallbacks.

Why Callismart HTTP?

Most modern PHP HTTP clients rely heavily on the cURL extension. However, in many shared hosting environments or restricted server setups, cURL may be disabled or outdated.

Callismart HTTP solves this by providing:

  • Adapter Fallbacks: If ext-curl isn't available, the client automatically fails over to fopen or socket communication.
  • Zero Hard Dependencies: Keep your project footprint small—ideal for WordPress plugins and CLI tools.
  • AWS SigV4: Native support for signing requests to AWS services (SES, S3, Lambda, etc.) without the weight of the full AWS SDK.
  • Immutable Value Objects: Predictable request and response objects that are thread-safe and cacheable.
  • Performance Tracking: Built-in latency measuring for every request.
  • Smart Streaming: Download large files directly to disk without buffering in memory.
  • Cookie Management: Automatic parsing and replay of session cookies across requests.

Installation

Install the package via Composer:

composer require callismart/http

Quick Start

Basic GET Request

use Callismart\Http\HttpClient;

$client   = new HttpClient();
$response = $client->get( 'https://api.example.com/v1/resource' );

if ( $response->ok() ) {
    $data = $response->json();
    print_r( $data );
}

POST Request with JSON

$response = $client->post_json( 'https://api.example.com/v1/users', [
    'name'  => 'Callistus',
    'role'  => 'Developer',
    'email' => 'callistus@example.com',
] );

if ( $response->is_success() ) {
    $user = $response->json();
    echo "Created user ID: {$user['id']}";
}

Download a File

Stream a large file directly to disk without holding it in memory:

$response = $client->download(
    'https://example.com/releases/plugin-2.1.0.zip',
    '/var/www/downloads/plugin-2.1.0.zip'
);

if ( $response->is_success() ) {
    echo "Saved {$response->file_size} bytes to {$response->sink_path}";
}

AWS Service Request (SES)

Sign and send a request to AWS:

use Callismart\Http\HttpRequest;
use Callismart\Http\AwsSignatureV4;

$request = HttpRequest::post( 'https://email.us-east-1.amazonaws.com/', $body )
    ->with_header( 'Content-Type', 'application/x-www-form-urlencoded' );

$signer   = new AwsSignatureV4( $access_key, $secret_key, 'us-east-1', 'ses' );
$signed   = $signer->sign( $request );
$response = $client->send( $signed );

Client Initialization

Auto-Detection (Recommended)

The client automatically detects the best available adapter in this priority order:

  1. cURL (ext-curl) — Highest performance, full feature support
  2. fopen (allow_url_fopen = On) — Fallback for shared hosting
  3. Socket (fsockopen) — Last resort, always available
$client = new HttpClient();
// Automatically uses the first available adapter

Explicit Adapter Selection

If you want to force a specific adapter:

use Callismart\Http\Adapters\CurlAdapter;
use Callismart\Http\Adapters\FopenAdapter;
use Callismart\Http\Adapters\SocketAdapter;

// Force cURL
$client = new HttpClient( new CurlAdapter() );

// Force fopen
$client = new HttpClient( new FopenAdapter() );

// Force socket (always available)
$client = new HttpClient( new SocketAdapter() );

Check Current Adapter

$adapter = $client->get_adapter();
echo "Using adapter: " . $adapter->get_id(); // "curl", "fopen", or "socket"

Set Default Headers

Persistent headers applied to every request (useful for authentication):

$client->with_default_header( 'Authorization', "Bearer {$token}" )
       ->with_default_header( 'User-Agent', 'MyApp/1.0' );

// These headers are merged into every request sent via this client
$response = $client->get( 'https://api.example.com/user' );
// Automatically includes the Authorization and User-Agent headers

Per-request headers always override client defaults.

Building Requests

Requests are immutable value objects built with a fluent API.

Named Constructors

use Callismart\Http\HttpRequest;

// GET
$request = HttpRequest::get( 'https://api.example.com/posts' );

// POST
$request = HttpRequest::post( 'https://api.example.com/posts', $body );

// PUT
$request = HttpRequest::put( 'https://api.example.com/posts/123', $body );

// PATCH
$request = HttpRequest::patch( 'https://api.example.com/posts/123', $body );

// DELETE
$request = HttpRequest::delete( 'https://api.example.com/posts/123' );

Fluent Withers

All withers return a new immutable request copy:

Add a Header

$request = HttpRequest::get( $url )
    ->with_header( 'Authorization', "Bearer {$token}" )
    ->with_header( 'X-Custom-Header', 'value' );

JSON Body & Headers

Automatically sets Content-Type and Accept to application/json:

$request = HttpRequest::post( $url )
    ->with_json( [
        'name'  => 'Alice',
        'email' => 'alice@example.com',
    ] );

// Equivalent to:
// $request = HttpRequest::post( $url, json_encode( [...] ) )
//     ->with_header( 'Content-Type', 'application/json' )
//     ->with_header( 'Accept', 'application/json' );

Add a Cookie

$request = HttpRequest::get( $url )
    ->with_cookie( 'session_id', 'abc123' )
    ->with_cookie( 'user_pref', 'dark_mode' );

// Cookies are automatically formatted into the Cookie header

Stream Response to File

Downloads the response body directly to disk without buffering:

$request = HttpRequest::get( 'https://example.com/large-file.zip' )
    ->with_sink( '/tmp/large-file.zip' );

$response = $client->send( $request );

if ( $response->is_success() ) {
    echo "File saved to: {$response->sink_path}";
    echo "File size: {$response->file_size} bytes";
}

The destination directory must exist and be writable. Throws InvalidArgumentException otherwise.

Disable SSL Verification

Use only in development or testing:

$request = HttpRequest::get( 'https://self-signed.example.com' )
    ->without_ssl_verification();

Apply Multiple Options

$request = HttpRequest::get( $url )->with_options( [
    'timeout'       => 60,           // seconds
    'verify_ssl'    => false,        // boolean
    'max_redirects' => 10,           // integer
    'cookies'       => [             // array of name => value
        'session_id' => 'xyz789',
    ],
] );

Sending Requests

Via HttpClient

$response = $client->send( $request );

Convenience Methods

HttpClient provides shorthand methods for common patterns:

// GET
$response = $client->get( 'https://api.example.com/users' );

// POST with body
$response = $client->post( 'https://api.example.com/users', $json_body );

// POST with JSON (automatic encoding + headers)
$response = $client->post_json( 'https://api.example.com/users', [
    'name' => 'Bob',
] );

// PUT
$response = $client->put( 'https://api.example.com/users/123', $body );

// PATCH
$response = $client->patch( 'https://api.example.com/users/123', $body );

// DELETE
$response = $client->delete( 'https://api.example.com/users/123' );

// Download (with automatic streaming)
$response = $client->download( $url, '/path/to/file' );

Handling Responses

Responses are immutable value objects with helper methods for common patterns.

Status Checks

$response = $client->get( $url );

// Shorthand for is_success()
if ( $response->ok() ) {
    // 2xx status
}

// Detailed checks
if ( $response->is_success() ) {
    // 200-299
}

if ( $response->is_client_error() ) {
    // 400-499
}

if ( $response->is_server_error() ) {
    // 500-599
}

if ( $response->is_error() ) {
    // 400-599
}

if ( $response->is_redirect() ) {
    // 300-399
}

Body Parsing

// JSON decoding
$data = $response->json(); // returns decoded array (or null if invalid)

// Check if body is valid JSON
if ( $response->is_json() ) {
    $data = $response->json();
}

// Raw body as string
$raw = $response->body;

Headers

// Get a single header (case-insensitive)
$content_type = $response->get_header( 'content-type' );

// Check if header exists
if ( $response->has_header( 'content-length' ) ) {
    // ...
}

// Shorthand for Content-Type
$type = $response->content_type(); // e.g. "application/json; charset=utf-8"

// All headers as associative array
$headers = $response->headers; // keys are lowercase

Cookies

// Get a single cookie
$session_id = $response->get_cookie( 'session_id' );

// Check if cookie exists
if ( $response->has_cookie( 'session_id' ) ) {
    // ...
}

// All cookies as associative array
$cookies = $response->cookies;

Redirect Tracking

// Check if any redirects were followed
if ( $response->was_redirected() ) {
    echo "Original URL: {$response->original_url()}";
    echo "Final URL: {$response->final_url()}";
}

// Complete redirect history (ordered list of URLs)
foreach ( $response->redirect_history as $url ) {
    echo "Followed: {$url}\n";
}

Download Responses

$response = $client->download( $url, '/tmp/file.zip' );

// Check if response was streamed to file
if ( $response->is_download() ) {
    echo "File: {$response->sink_path}";
    echo "Size: {$response->file_size} bytes";
    echo "Body is empty: " . ( $response->body === '' ? 'yes' : 'no' );
}

For buffered responses, save to file later:

$response = $client->get( $url ); // buffered in memory

if ( $response->is_success() ) {
    $response->save_to( '/tmp/file.zip' ); // write to disk
}

Performance Metrics

$response = $client->get( $url );

echo "Request took {$response->latency} seconds";

HTTP Methods with Examples

GET

Retrieve data from a server:

$response = $client->get( 'https://api.example.com/posts/42' );

if ( $response->is_success() ) {
    $post = $response->json();
    echo "Title: {$post['title']}";
}

POST

Submit data and create resources:

$response = $client->post_json( 'https://api.example.com/posts', [
    'title'   => 'Hello World',
    'content' => 'This is my first post.',
    'author'  => 'Alice',
] );

if ( $response->is_success() ) {
    $created = $response->json();
    echo "Post created with ID: {$created['id']}";
}

Custom headers:

$request = HttpRequest::post( $url, $body )
    ->with_header( 'Content-Type', 'application/x-www-form-urlencoded' )
    ->with_header( 'X-API-Key', 'secret' );

$response = $client->send( $request );

PUT

Replace an entire resource:

$response = $client->put_json( 'https://api.example.com/posts/42', [
    'title'   => 'Updated Title',
    'content' => 'Updated content.',
    'author'  => 'Alice',
] );

PATCH

Partially update a resource:

$response = $client->patch_json( 'https://api.example.com/posts/42', [
    'title' => 'New Title Only',
] );

DELETE

Remove a resource:

$response = $client->delete( 'https://api.example.com/posts/42' );

if ( $response->is_success() ) {
    echo "Post deleted";
}

JSON Support

Automatic Encoding

The with_json() method automatically encodes your data and sets the correct headers:

$request = HttpRequest::post( $url )
    ->with_json( [ 'key' => 'value' ] );

// Headers set automatically:
// Content-Type: application/json
// Accept: application/json

Automatic Decoding

The json() method safely decodes JSON responses:

$response = $client->post_json( $url, $data );

// Returns decoded array (or null if invalid/empty)
$result = $response->json();

// Check validity first
if ( $response->is_json() ) {
    $result = $response->json();
} else {
    echo "Response is not valid JSON";
}

File Downloads

Stream to Disk (Recommended for Large Files)

Stream the response body directly to a file without buffering in memory:

$response = $client->download(
    'https://example.com/releases/large-file-100mb.zip',
    '/var/downloads/large-file.zip'
);

if ( $response->is_success() ) {
    echo "Downloaded {$response->file_size} bytes";
} else {
    echo "Download failed with status {$response->status_code}";
}

The file is created at download time. On error, partially downloaded files are automatically cleaned up.

Buffer in Memory (Small Files)

For small responses, buffer in memory and save later:

$response = $client->get( 'https://example.com/image.png' );

if ( $response->is_success() ) {
    $response->save_to( '/var/downloads/image.png' );
}

Request-Level Sink

Manual sink configuration via request:

$request = HttpRequest::get( $url )->with_sink( '/tmp/download.zip' );
$response = $client->send( $request );

if ( $response->is_download() ) {
    echo "Saved to {$response->sink_path}";
}

AWS Signature Version 4

Sign requests to AWS services without the full AWS SDK:

Setup

use Callismart\Http\AwsSignatureV4;

$signer = new AwsSignatureV4(
    access_key: 'AKIAIOSFODNN7EXAMPLE',
    secret_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
    region:     'us-east-1',
    service:    'ses', // or 's3', 'lambda', etc.
);

Sign a Request

The sign() method returns a new request with AWS signing headers applied:

$request = HttpRequest::post( 'https://email.us-east-1.amazonaws.com/', $body )
    ->with_header( 'Content-Type', 'application/x-www-form-urlencoded' );

$signed = $signer->sign( $request );

// Headers automatically added:
// - X-Amz-Date: 20250512T123456Z
// - X-Amz-Content-Sha256: (SHA256 hash of body)
// - Authorization: AWS4-HMAC-SHA256 Credential=..., SignedHeaders=..., Signature=...

$response = $client->send( $signed );

Headers Added by the Signer

The signer adds or overwrites three headers:

  • X-Amz-Date: Request timestamp in YYYYMMDDTHHmmssZ format (UTC)
  • X-Amz-Content-Sha256: SHA256 hash of the request body (hex-encoded)
  • Authorization: AWS4-HMAC-SHA256 signature string containing credentials, signed headers, and signature

All other headers (including custom ones) are preserved and included in the signature.

Example: AWS SES

Send an email via AWS SES:

$body = http_build_query( [
    'Action'                   => 'SendEmail',
    'Source'                   => 'noreply@example.com',
    'Destination.ToAddresses.1' => 'user@example.com',
    'Message.Subject.Data'      => 'Hello!',
    'Message.Body.Text.Data'    => 'This is a test email.',
] );

$request = HttpRequest::post( 'https://email.us-east-1.amazonaws.com/', $body )
    ->with_header( 'Content-Type', 'application/x-www-form-urlencoded' );

$signer   = new AwsSignatureV4( $key, $secret, 'us-east-1', 'ses' );
$signed   = $signer->sign( $request );
$response = $client->send( $signed );

if ( $response->is_success() ) {
    $xml = $response->body;
    // Parse XML response...
}

Example: AWS S3

Sign a request to S3 (Get Object):

$request = HttpRequest::get( 'https://my-bucket.s3.amazonaws.com/config.json' );

$signer   = new AwsSignatureV4( $key, $secret, 'us-east-1', 's3' );
$signed   = $signer->sign( $request );
$response = $client->send( $signed );

if ( $response->is_success() ) {
    $config = $response->json();
}

Adapters

The HTTP client uses a three-tier adapter system for maximum compatibility:

cURL Adapter

Availability: When ext-curl is loaded Features: Full support — redirects, SSL verification, cookies, timeouts Performance: Highest — uses native C implementation

use Callismart\Http\Adapters\CurlAdapter;

$adapter = new CurlAdapter();
if ( $adapter->is_available() ) {
    echo "cURL is available";
}

fopen Adapter

Availability: When allow_url_fopen = On in php.ini Features: Redirects, SSL verification, cookies, timeouts Limitation: May not work with restrictive open_basedir settings

use Callismart\Http\Adapters\FopenAdapter;

$adapter = new FopenAdapter();
if ( $adapter->is_available() ) {
    echo "fopen streaming is available";
}

Socket Adapter

Availability: Always (when fsockopen function exists) Features: Redirects (manual), SSL via ssl:// wrapper, cookies Limitation: No proxy support, manual socket handling

use Callismart\Http\Adapters\SocketAdapter;

$adapter = new SocketAdapter();
if ( $adapter->is_available() ) {
    echo "Socket fallback is available";
}

Automatic Selection

The HttpClient tests adapters in priority order and uses the first available:

$client = new HttpClient();
// Automatically selects cURL → fopen → socket

// If none are available, throws HttpRequestException:
// "HttpClient: no HTTP adapter is available in this environment. 
//  Enable cURL, allow_url_fopen, or fsockopen."

Manual Selection

Force a specific adapter:

$client = new HttpClient( new SocketAdapter() );
// Uses socket even if cURL is available

Error Handling

Exception Hierarchy

RuntimeException
  └─ HttpRequestException
      └─ HttpTimeoutException

Catch Exceptions

use Callismart\Http\Exceptions\HttpRequestException;
use Callismart\Http\Exceptions\HttpTimeoutException;

try {
    $response = $client->get( $url );
} catch ( HttpTimeoutException $e ) {
    // Request exceeded timeout
    echo "Timeout: {$e->getMessage()}";
} catch ( HttpRequestException $e ) {
    // Connection failed, DNS error, socket error, etc.
    echo "Error: {$e->getMessage()}";
}

Common Errors

Error Cause
HttpTimeoutException Request exceeded configured timeout
HttpRequestException (DNS) Domain could not be resolved
HttpRequestException (connection) TCP connection refused or timeout
HttpRequestException (SSL) SSL certificate verification failed (when verify_ssl=true)
InvalidArgumentException Invalid URL, unsupported HTTP method, missing sink directory

Configuration Options

Request Options

$request = HttpRequest::get( $url )->with_options( [
    'timeout'       => 30,       // seconds (default: 30)
    'verify_ssl'    => true,     // boolean (default: true)
    'max_redirects' => 5,        // integer (default: 5)
    'cookies'       => [],       // array of name => value
    'sink'          => null,     // absolute path for streaming
] );

Client Configuration

$client = new HttpClient();

// Set default headers
$client->with_default_header( 'Authorization', "Bearer {$token}" );
$client->with_default_header( 'User-Agent', 'MyApp/1.0' );

// Check current adapter
$adapter_id = $client->get_adapter()->get_id(); // "curl", "fopen", or "socket"

Requirements

  • PHP: 8.1 or higher
  • ext-openssl: Recommended for HTTPS support
  • At least one of:
    • ext-curl (preferred)
    • allow_url_fopen = On in php.ini
    • fsockopen() function enabled

Performance Tips

  1. Use Streaming for Large Files: Use with_sink() or download() to avoid buffering entire files in memory.

  2. Reuse HttpClient Instances: Create once, send many requests through it to benefit from connection pooling (cURL).

  3. Set Appropriate Timeouts: Balance between catching truly broken connections and allowing slow networks:

    $request = HttpRequest::get( $url )->with_options( [ 'timeout' => 60 ] );
  4. Disable SSL Verification Only in Development: Use without_ssl_verification() only in trusted environments.

  5. Check Redirect Loops: Monitor redirect_history to detect infinite redirects:

    if ( count( $response->redirect_history ) > 10 ) {
        // Possible redirect loop
    }

License

This project is licensed under the MIT License. See LICENSE file for details.

Author

Callistus Nwachukwu

Contributing

Contributions are welcome. Please ensure all code follows WordPress PHP Coding Standards (K&R braces, spaces inside parentheses, tab indentation).

Support

For issues, feature requests, or questions, please visit the project repository.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 5
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-05-12

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固