mrizwan/laravel-fcgi-client

A Laravel package for communicating with FastCGI-compatible servers (like PHP-FPM)

v1.0.1 2025-05-26 23:06 UTC

README

A modern, Laravel-style FastCGI client that lets your Laravel application communicate directly with FastCGI-compatible servers like PHP-FPM.

Acknowledgements

This package is built as a fork of the fast-cgi-client library originally authored by hollodotme. We've adapted and extended it to provide seamless integration with Laravelβ€”many thanks to the original author for creating such a solid foundation.

What is FastCGI?

FastCGI is a protocol that allows a web server (like Nginx or Apache) to communicate with external applications, typically programming language interpreters like PHP-FPM (PHP FastCGI Process Manager). It's an improvement over the older CGI protocol, offering better performance through persistent processes.

In a traditional PHP web application setup, the web server (e.g., Nginx) receives HTTP requests and forwards them to PHP-FPM via FastCGI, which then executes the PHP scripts and returns the results.

Where This Package Can Be Used

This package enables Laravel applications to directly communicate with PHP-FPM, which opens up several use cases:

  1. Microservices Architecture: Directly communicate with other PHP-based microservices without going through HTTP, reducing overhead.

  2. Private API Access: Access internal APIs on remote servers via FastCGI instead of exposing them through HTTP endpoints.

  3. Cross-Application Communication: Call scripts on another PHP application from your Laravel app with lower latency than HTTP requests.

  4. Gateway/Proxy Services: Create gateway services that can route requests to appropriate backend PHP services.

πŸš€ Installation

composer require mrizwan/laravel-fcgi-client

πŸ”§ Configuration

The package automatically registers the service provider and facade in Laravel 9.x and above.

You can use the facade in your code by importing it:

use Rizwan\LaravelFcgiClient\Facades\FCGI;

βœ… Basic Usage

Simple GET Request

$response = FCGI::withUri('/api/products')
    ->withQuery(['category' => 'electronics'])
    ->get('remote-server.com', '/var/www/public/index.php', [], 9000);

// Handle the response
$data = $response->json();
$statusCode = $response->getStatusCode();

POST Request with Data

$response = FCGI::withUri('/api/products')
    ->post('remote-server.com', '/var/www/public/index.php', [
        'name' => 'New Product',
        'price' => 99.99,
        'category' => 'electronics'
    ]);

// Check if the request was successful
if ($response->successful()) {
    $newProduct = $response->json();
    echo "Created product with ID: " . $newProduct['id'];
}

JSON Request

$response = FCGI::withUri('/api/users')
    ->asJson()
    ->post('auth-service.internal', '/var/www/public/index.php', [
        'user' => [
            'name' => 'John Doe',
            'email' => 'john@example.com'
        ]
    ]);

πŸ“‘ Available Methods

HTTP Methods

All HTTP methods support Laravel-like signatures with optional data arrays:

  • get(string $host, string $scriptPath, array $query = [], ?int $port = null)
  • post(string $host, string $scriptPath, array $data = [], ?int $port = null)
  • put(string $host, string $scriptPath, array $data = [], ?int $port = null)
  • patch(string $host, string $scriptPath, array $data = [], ?int $port = null)
  • delete(string $host, string $scriptPath, array $data = [], ?int $port = null)

Request Configuration

// Add HTTP headers
FCGI::withHeaders([
    'Authorization' => 'Bearer your-token',
    'Accept-Language' => 'en-US'
]);

// Add a single header
FCGI::withHeader('X-API-Key', 'secret-key');

// Add query parameters (for GET requests or as method parameter)
FCGI::withQuery([
    'page' => 1,
    'limit' => 20,
    'sort' => 'created_at'
]);

// Add request payload (for POST, PUT, PATCH requests)
FCGI::withPayload([
    'name' => 'Product Name',
    'description' => 'Product description'
]);

// Set raw body content with content type
FCGI::withBody(
    '{"custom":"json structure"}',
    'application/json'
);

// Set the URI with URL template support
FCGI::withUri('/api/v1/users/{userId}/posts/{postId}');

// Set URL parameters for template substitution
FCGI::withUrlParameters([
    'userId' => 123,
    'postId' => 456,
    'user' => ['id' => 789] // Supports nested parameters
]);

// Add custom server parameters that will be available in $_SERVER
FCGI::withServerParams([
    'HTTP_X_CUSTOM_HEADER' => 'Value',
    'SERVER_NAME' => 'custom-server'
]);

// Add custom FastCGI variables
FCGI::withCustomVars([
    'CUSTOM_VAR' => 'custom_value'
]);

// Set connection timeouts (in seconds)
FCGI::timeout(30);        // Read timeout
FCGI::connectTimeout(5);  // Connect timeout

// Add retry logic
FCGI::retry(
    3,                      // Number of retries
    500,                    // Delay between retries in milliseconds
    function ($exception) { // Optional callback to determine whether to retry
        return !($exception instanceof AuthenticationException);
    }
);

Content Type Configuration

// Configure request to send JSON payload
FCGI::asJson()
    ->post('service.com', '/script.php', ['key' => 'value']);

// Configure request to send form-encoded data
FCGI::asForm()
    ->post('service.com', '/script.php', ['key' => 'value']);

// Set Accept header to JSON
FCGI::acceptJson();

// Set Accept header to specific content type
FCGI::accept('application/xml');

// Set custom content type
FCGI::contentType('application/vnd.api+json');

Authentication

// Bearer token authentication
FCGI::withToken('your-jwt-token')
    ->get('api.example.com', '/protected/endpoint.php');

// Custom token type
FCGI::withToken('api-key-123', 'ApiKey')
    ->get('api.example.com', '/endpoint.php');

// Basic authentication
FCGI::withBasicAuth('username', 'password')
    ->get('api.example.com', '/endpoint.php');

// Custom User-Agent
FCGI::withUserAgent('MyApp/1.0')
    ->get('api.example.com', '/endpoint.php');

Method Chaining

You can chain multiple methods together for cleaner code:

$response = FCGI::withHeaders(['X-API-Key' => 'secret-key'])
    ->withUri('/api/v1/users/{userId}')
    ->withUrlParameters(['userId' => 123])
    ->withQuery(['include' => 'profile'])
    ->timeout(10)
    ->retry(3, 1000)
    ->get('user-service.internal', '/var/www/public/index.php');

πŸ”— URL Templates

The package supports URL templates with parameter substitution:

// Simple parameter substitution
$response = FCGI::withUri('/api/users/{id}')
    ->withUrlParameters(['id' => 123])
    ->get('api.example.com', '/index.php');
// Results in: /api/users/123

// Nested parameter support
$response = FCGI::withUri('/api/users/{user.id}/posts/{post.id}')
    ->withUrlParameters([
        'user' => ['id' => 123],
        'post' => ['id' => 456]
    ])
    ->get('api.example.com', '/index.php');
// Results in: /api/users/123/posts/456

// Parameters not found remain as placeholders
$response = FCGI::withUri('/api/{missing}/endpoint')
    ->withUrlParameters(['existing' => 'value'])
    ->get('api.example.com', '/index.php');
// Results in: /api/{missing}/endpoint

🌐 Response API

The response object provides a rich API similar to Laravel's HTTP client:

$response = FCGI::get('api.example.com', '/script.php');

// Status code and state checks
$statusCode = $response->getStatusCode();       // e.g. 200
$isOk = $response->ok();                       // true if 200
$isUnauthorized = $response->unauthorized();   // true if 401
$isForbidden = $response->forbidden();         // true if 403
$isNotFound = $response->notFound();           // true if 404
$isServerError = $response->serverError();     // true if 5xx
$isSuccessful = $response->successful();       // true if < 400 and no error

// Content access
$body = $response->body();                     // Raw body string
$data = $response->json();                     // Decoded JSON array
$value = $response->json('user.name');         // Access JSON via dot notation
$array = $response->toArray();                 // Response as array including timing metrics

// Headers
$contentType = $response->header('Content-Type');  // Get a single header
$allHeaders = $response->getHeaders();            // Get all headers
$hasHeader = $response->hasHeader('X-Custom');    // Check if header exists

// Performance
$duration = $response->getDuration();             // Response read duration in seconds
$connectTime = $response->getConnectDuration();   // TCP connect duration in milliseconds
$writeTime = $response->getWriteDuration();       // Request write duration in milliseconds
$attempts = $response->getAttempts();             // Number of retry attempts made

// Exception handling
$response->throw();                               // Throws if status >= 400
$response->throwIf($condition);                   // Conditional throw 
$response->throwUnless($condition);               // Conditional throw

πŸ” Concurrent Requests with Laravel's Concurrency Facade

For Laravel 11+, you can use Laravel's built-in Concurrency facade to execute multiple FastCGI requests in parallel:

use Illuminate\Support\Facades\Concurrency;

$responses = Concurrency::concurrent([
    'products' => fn() => FCGI::withUri('/api/products')
        ->get('product-service.internal', '/var/www/public/index.php'),
        
    'categories' => fn() => FCGI::withUri('/api/categories')
        ->get('catalog-service.internal', '/var/www/public/index.php'),
        
    'user' => fn() => FCGI::withUri('/api/user')
        ->withToken($token)
        ->get('user-service.internal', '/var/www/public/index.php'),
]);

// Access responses by key
$products = $responses['products']->json();
$categories = $responses['categories']->json();
$user = $responses['user']->json();

Real-World Examples

Accessing a Remote API with URL Templates

$response = FCGI::withHeaders([
    'Authorization' => 'Bearer ' . $apiToken,
    'Accept' => 'application/json',
])
->withUri('/api/v1/users/{userId}/stats/daily')
->withUrlParameters(['userId' => auth()->id()])
->timeout(5)
->get('api-backend.internal', '/var/www/html/api/public/index.php');

return $response->json();

Submitting a Form to a Remote Application

$response = FCGI::withUri('/submit-form')
    ->asForm()
    ->post('contact-service.internal', '/var/www/html/contact/public/index.php', [
        'email' => $request->email,
        'name' => $request->name,
        'message' => $request->message,
    ]);

if ($response->successful()) {
    return redirect()->back()->with('success', 'Your message has been sent!');
} else {
    return redirect()->back()->with('error', 'Failed to send message.');
}

Creating a Resource on a Remote Service

$response = FCGI::withHeaders([
    'X-API-Key' => config('services.blog.api_key'),
])
->withUri('/api/posts')
->asJson()
->post('blog-service.internal', '/var/www/html/blog/public/index.php', [
    'title' => $request->title,
    'content' => $request->content,
    'author_id' => Auth::id(),
]);

return response()->json($response->json(), $response->getStatusCode());

RESTful API with URL Templates

// GET /api/users/123/posts/456/comments
$response = FCGI::withUri('/api/users/{userId}/posts/{postId}/comments')
    ->withUrlParameters([
        'userId' => $user->id,
        'postId' => $post->id
    ])
    ->withQuery(['include' => 'author,replies'])
    ->get('blog-service.internal', '/var/www/blog/public/index.php');

// PUT /api/users/123/profile
$response = FCGI::withUri('/api/users/{userId}/profile')
    ->withUrlParameters(['userId' => $user->id])
    ->asJson()
    ->put('user-service.internal', '/var/www/user/public/index.php', [
        'name' => $request->name,
        'bio' => $request->bio
    ]);

// DELETE /api/posts/456
$response = FCGI::withUri('/api/posts/{postId}')
    ->withUrlParameters(['postId' => $post->id])
    ->delete('blog-service.internal', '/var/www/blog/public/index.php');

Fetching Data from Multiple Services

use Illuminate\Support\Facades\Concurrency;

$user = auth()->user();

$dashboard = Concurrency::concurrent([
    'profile' => fn() => FCGI::withUri('/api/users/{userId}')
        ->withUrlParameters(['userId' => $user->id])
        ->get('user-service.internal', '/var/www/user-service/public/index.php'),
        
    'orders' => fn() => FCGI::withUri('/api/users/{userId}/orders')
        ->withUrlParameters(['userId' => $user->id])
        ->withQuery(['status' => 'active'])
        ->get('order-service.internal', '/var/www/order-service/public/index.php'),
        
    'notifications' => fn() => FCGI::withUri('/api/users/{userId}/notifications')
        ->withUrlParameters(['userId' => $user->id])
        ->withQuery(['unread' => true])
        ->get('notification-service.internal', '/var/www/notification-service/public/index.php'),
]);

return view('dashboard', [
    'profile' => $dashboard['profile']->json(),
    'orders' => $dashboard['orders']->json(),
    'notificationCount' => count($dashboard['notifications']->json('data')),
]);

Smart Content Type Defaults

The package intelligently chooses content types based on the HTTP method:

// POST requests default to form-encoded data
FCGI::post('service.com', '/script.php', ['key' => 'value']);
// Content-Type: application/x-www-form-urlencoded

// PUT/PATCH requests default to JSON
FCGI::put('service.com', '/script.php', ['key' => 'value']);
// Content-Type: application/json

// Override defaults with explicit configuration
FCGI::asJson()->post('service.com', '/script.php', ['key' => 'value']);
// Content-Type: application/json

FCGI::asForm()->put('service.com', '/script.php', ['key' => 'value']);
// Content-Type: application/x-www-form-urlencoded

Error Handling

The package provides several ways to handle errors:

// Using try-catch blocks
try {
    $response = FCGI::get('service.internal', '/path/to/script.php');
    return $response->json();
} catch (\Rizwan\LaravelFcgiClient\Exceptions\ConnectionException $e) {
    // Handle connection errors (e.g., service unreachable)
    Log::error('FastCGI connection failed: ' . $e->getMessage());
    return response()->json(['error' => 'Service unavailable'], 503);
} catch (\Rizwan\LaravelFcgiClient\Exceptions\TimeoutException $e) {
    // Handle timeout errors
    Log::error('FastCGI request timed out: ' . $e->getMessage());
    return response()->json(['error' => 'Request timed out'], 504);
} catch (\Rizwan\LaravelFcgiClient\Exceptions\FastCGIException $e) {
    // Handle all other FastCGI-related errors
    Log::error('FastCGI error: ' . $e->getMessage());
    return response()->json(['error' => 'Internal server error'], 500);
}

// Using the throw method and Laravel's error handling
$posts = FCGI::get('blog-service.internal', '/var/www/blog/public/index.php')
    ->throw()  // This will throw an exception if the request fails
    ->json();

// Using conditional throws
$response = FCGI::get('service.internal', '/path/to/script.php');

$response->throwIf(
    $response->getStatusCode() === 429,
    fn() => new RateLimitException('Too many requests')
);

// Using throwUnless
$response->throwUnless(
    $response->getStatusCode() === 200,
    fn() => new ServiceException('Expected 200 OK response')
);

Retry Logic

For unstable services or network connections, you can add retry logic with intelligent defaults:

// Basic retry (3 attempts with 500ms delay)
$response = FCGI::retry(3, 500)
    ->get('flaky-service.internal', '/var/www/public/index.php');

// By default, retries happen on:
// - Connection failures and timeouts (always)
// - Server errors (5xx status codes)
// - NOT on client errors (4xx status codes)

// Custom retry logic based on the exception or response
$response = FCGI::retry(3, 1000, function ($exceptionOrResponse, $request) {
    // Only retry for connection and timeout issues
    if ($exceptionOrResponse instanceof \Throwable) {
        return $exceptionOrResponse instanceof ConnectionException || 
               $exceptionOrResponse instanceof TimeoutException;
    }
    
    // For responses, retry on server errors but not client errors
    if ($exceptionOrResponse instanceof Response) {
        return $exceptionOrResponse->serverError();
    }
    
    return false;
})
->withToken($token)
->get('api-service.internal', '/var/www/public/index.php');

// Check retry attempts
$response = FCGI::retry(3)->get('service.com', '/script.php');
echo "Request took {$response->getAttempts()} attempts";

πŸ§ͺ Testing

Tests are written using Pest PHP. Run them with:

./vendor/bin/pest

πŸ“œ License

The MIT License (MIT). Please see License File for more information.