jooservices/jooclient

Wrapper for Guzzle

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 1

pkg:composer/jooservices/jooclient

1.0.1 2025-11-08 07:26 UTC

This package is auto-updated.

Last update: 2025-11-08 08:32:04 UTC


README

PHP Version Laravel Version Tests Assertions Coverage

A production-ready, enterprise-grade Guzzle HTTP client wrapper designed specifically for Laravel 12 with PHP 8.4. Features pluggable logging (MySQL, MongoDB, Monolog), intelligent retry logic, caching support, and comprehensive testing utilities.

Features

Core Features

  • Immutable Factory Pattern - Thread-safe, predictable client creation
  • Auto-Detection Logging ⭐ - Just enable drivers you want, system handles the rest
  • Multi-Driver Logging - MySQL, MongoDB, or Monolog with automatic request/response capture
  • LoggingManager - Log to one or multiple destinations simultaneously
  • Response Wrapper ⭐ NEW - Client::request() returns a helper with isSuccess() and getContent()
  • Redis & Filesystem Caching - HTTP response caching with configurable TTL
  • Intelligent Retry Logic - Exponential backoff with configurable error codes
  • Desktop User-Agent Rotation ⭐ NEW - Random desktop signatures per request, stable across retries via jooservices/jooagent
  • Batch Processing - Efficient bulk log writes with transaction support
  • File Rotation - Automatic log file rotation with size limits

Security & Compliance

  • 🔒 Data Sanitization ⭐ NEW - GDPR & PCI-DSS compliant logging
  • 🔒 Sensitive Data Protection - Auto-redacts passwords, API keys, credit cards
  • 🔒 Configurable Sanitization - Custom sensitive field patterns
  • 🔒 Secure by Default - Sanitization enabled out of the box

Monitoring & Observability

  • 📊 Metrics Collection ⭐ NEW - Track performance, cache hit rates, error rates
  • 🏥 Health Checks ⭐ NEW - Verify MySQL, MongoDB, Redis, filesystem availability
  • 📈 Performance Tracking - Request duration, memory usage, system metrics

Developer Experience

  • 👨‍💻 Code Examples ⭐ NEW - 6 ready-to-run examples
  • 🤖 CI/CD Pipeline ⭐ NEW - Automated testing with GitHub Actions
  • 📚 Comprehensive Docs - 18 guides covering all features
  • 177 Tests - 540 assertions covering all edge cases
  • SOLID Principles - Clean architecture with full OOP compliance
  • Laravel Integration - Native service provider, DI support, facade-ready

🚀 What's New in 1.0.1

  • Random Desktop User-Agents – Every outgoing request now defaults to a realistic desktop user-agent generated by jooagent, while retries reuse the same signature for consistency.
  • Factory::enableRandomUserAgent() – Toggle the behavior or inject a custom DesktopUserAgentSession for fine-grained control.
  • Documentation & Tests – Added coverage and usage notes for the user-agent middleware.

🚀 What's New in 1.1.0

  • Client::request() and Client::get() now return a ResponseWrapper with convenient helpers.
  • New ResponseWrapper::isSuccess() and ResponseWrapper::getContent() eliminate boilerplate status checks and body handling.
  • ResponseWrapper::getContent() auto-converts JSON responses to arrays, wraps HTML in a DomWrapper (Symfony DomCrawler) and returns null for non-2xx responses.
  • Documentation updated with new usage patterns and refreshed test statistics.

📚 Documentation

Getting Started

Features & Guides

Operations & Monitoring

Reference

Requirements

  • PHP 8.4+
  • Laravel 12.x
  • MySQL 8.0+ OR MongoDB 6.0+ (optional, based on logging driver choice)
  • Redis 6.0+ OR Filesystem storage (optional, based on cache driver choice)

Installation

composer require jooservices/jooclient

Publish Configuration

php artisan vendor:publish --provider="JOOservices\Client\Providers\JooclientServiceProvider" --tag=config

Publish Migrations (for MySQL logging)

php artisan vendor:publish --provider="JOOservices\Client\Providers\JooclientServiceProvider" --tag=migrations
php artisan migrate

Quick Start

Basic Usage

use JOOservices\Client\Factory\Factory;

// Create a simple client
$factory = new Factory();
$result = $factory->make();
$response = $result->get('https://api.example.com/users');

if ($response->isSuccess()) {
    $usersJson = $response->getContent();
}

// Need the raw Guzzle client? It's still available via $result->client.

// Other HTTP verbs return wrappers too
$response = $result->post('/api/data', [
    'json' => ['name' => 'Example'],
]);

Via Laravel Service Container

// In a controller
public function __construct(
    private Factory $jooclient
) {}

public function getData()
{
    $result = $this->jooclient->make();
    $response = $result->get('/api/data');

    if (! $response->isSuccess()) {
        abort(502, 'Upstream service failed');
    }

    return response()->json($response->getContent());
}

With Auto-Detection Logging and Caching

// Configure via .env - simple and intuitive!
// JOOCLIENT_LOGGING_ENABLED=true
// JOOCLIENT_DB_LOGGING=true       # Enable MySQL
// JOOCLIENT_MONGODB_LOGGING=true  # Enable MongoDB
// JOOCLIENT_CACHE_ENABLED=true
// JOOCLIENT_CACHE_DRIVER=redis

// System automatically detects and uses multi-logger!
$factory = (new Factory())
    ->enableLogging() // Auto-detects MySQL + MongoDB
    ->enableCache();   // Redis caching

$result = $factory->make();
$response = $result->client->get('https://api.example.com'); // Logged to both MySQL & MongoDB, cached!
$result->flushLogger();

Configuration

All configuration is done via .env and config/jooclient.php. The package automatically loads settings from your Laravel environment.

Environment Variables

# Logging (Main Settings)
JOOCLIENT_LOGGING_ENABLED=true
JOOCLIENT_LOGGING_DRIVER=mysql  # mysql, mongodb, monolog, or multi

# Multi-Logger (when driver=multi)
JOOCLIENT_MULTI_DRIVERS=mysql,mongodb,monolog  # CSV list of drivers

# MySQL Logging
JOOCLIENT_DB_LOGGING=true
JOOCLIENT_DB_HOST=127.0.0.1
JOOCLIENT_DB_PORT=3306
JOOCLIENT_DB_DATABASE=jooclient
JOOCLIENT_DB_USERNAME=root
JOOCLIENT_DB_PASSWORD=secret
JOOCLIENT_DB_TABLE=client_request_logs
JOOCLIENT_DB_BATCH=false
JOOCLIENT_DB_FALLBACK=error_log

# MongoDB Logging
JOOCLIENT_MONGODB_LOGGING=true
JOOCLIENT_MONGODB_DSN=mongodb://127.0.0.1:27017
JOOCLIENT_MONGODB_DATABASE=jooclient

# Caching
JOOCLIENT_CACHE_ENABLED=true
JOOCLIENT_CACHE_DRIVER=redis  # redis or filesystem
JOOCLIENT_CACHE_TTL=3600  # 1 hour

# Redis Cache (when driver=redis)
JOOCLIENT_REDIS_HOST=127.0.0.1
JOOCLIENT_REDIS_PORT=6379
JOOCLIENT_REDIS_PASSWORD=
JOOCLIENT_REDIS_DATABASE=0
JOOCLIENT_REDIS_PREFIX=jooclient:

# Filesystem Cache (when driver=filesystem)
JOOCLIENT_CACHE_PATH=/path/to/cache
JOOCLIENT_MONGODB_COLLECTION=client_request_logs
JOOCLIENT_MONGODB_BATCH=false

# Retries
JOOCLIENT_RETRIES=true
JOOCLIENT_RETRIES_MAX=3
JOOCLIENT_RETRIES_DELAY=1
JOOCLIENT_RETRIES_MIN_ERROR_CODE=500

# Defaults
JOOCLIENT_TIMEOUT=30

Configuration File

The package includes a publishable config file with detailed options:

// config/jooclient.php
return [
    'logging' => [
        'enabled' => true,
        'driver' => 'mysql', // 'mysql', 'mongodb', 'monolog', or 'multi'

        // Multi-logger mode: log to multiple destinations simultaneously
        'multi_drivers' => ['mysql', 'mongodb'], // Used when driver is 'multi'

        'connection' => [
            'mysql' => [
                'enabled' => true,
                'host' => '127.0.0.1',
                'port' => 3306,
                'database' => 'jooclient',
                'username' => 'root',
                'password' => 'secret',
                'table' => 'client_request_logs',
                'batch' => false,
            ],
            'mongodb' => [
                'enabled' => true,
                'dsn' => 'mongodb://127.0.0.1:27017',
                'database' => 'jooclient',
                'collection' => 'client_request_logs',
                'batch' => false,
            ],
        ],
    ],
    'retries' => [
        'enabled' => true,
        'max_attempts' => 3,
        'delay_seconds' => 1,
        'min_error_code' => 500,
    ],
    'defaults' => [
        'timeout' => 30,
        'headers' => [
            // Optional: override the random desktop user-agent middleware
            'User-Agent' => 'JOOClient/1.0',
        ],
    ],
];

ℹ️ The random desktop user-agent middleware runs by default. Provide a static User-Agent header on the request options if you prefer to manage the value manually.

Usage Examples

MySQL Logging

use JOOservices\Client\Factory\Factory;

// Set up via .env:
// JOOCLIENT_LOGGING_ENABLED=true
// JOOCLIENT_LOGGING_DRIVER=mysql

$factory = (new Factory())
    ->addOptions(['timeout' => 30, 'base_uri' => 'https://api.example.com'])
    ->enableRetries(3, 1, 500)
    ->enableLogging(); // Loads config automatically from config/jooclient.php

$result = $factory->make();
$response = $result->client->get('/users');

// Important: Flush batched logs if batch mode is enabled
$result->flushLogger();

MongoDB Logging

// Set up via .env:
// JOOCLIENT_LOGGING_ENABLED=true
// JOOCLIENT_LOGGING_DRIVER=mongodb
// JOOCLIENT_MONGODB_DSN=mongodb://127.0.0.1:27017
// JOOCLIENT_MONGODB_DATABASE=jooclient

$factory = (new Factory())
    ->enableLogging(); // Loads MongoDB config automatically

$result = $factory->make();
$response = $result->client->post('/api/data', [
    'json' => ['name' => 'Test']
]);

$result->flushLogger();

Multi-Logger (Log to Multiple Destinations) ⭐ NEW

// config/jooclient.php
'logging' => [
    'driver' => 'multi',
    'multi_drivers' => ['mysql', 'mongodb'], // Log to both
    'connection' => [
        'mysql' => ['enabled' => true],
        'mongodb' => [
            'enabled' => true,
            'dsn' => 'mongodb://127.0.0.1:27017',
            'database' => 'jooclient',
            'collection' => 'client_request_logs',
        ],
    ],
],

// In your code - no changes needed!
$factory = Jooclient::fromConfig(config('jooclient'));
$result = $factory->make();
$response = $result->client->get('/api/endpoint');
$result->flushLogger(); // Flushes all loggers

Benefits:

  • Data redundancy (if one DB fails, other still works)
  • Error isolation (one logger failure doesn't affect others)
  • Flexible (different DBs for different purposes)

See MULTI_LOGGER_GUIDE.md for complete documentation.

Monolog Logging

use JOOservices\Client\Logging\Drivers\MonologLoggingAdapter;

$adapter = MonologLoggingAdapter::createFromConfig([
    'channel' => 'api_client',
    'file' => storage_path('logs/api-client.log'),
    'level' => 'debug',
    'formatter' => 'json',
]);

$formatter = new \GuzzleHttp\MessageFormatter();
$middleware = \JOOservices\Client\Logging\Middlewares\MonologLoggingMiddlewareFactory::create(
    $adapter,
    $formatter
);

$factory = (new Factory())->addMiddleware($middleware, 'monolog');
$result = $factory->make();

Retry Logic

// Retry failed requests (5xx errors) up to 3 times with 2 second delay
$factory = (new Factory())
    ->enableRetries(
        maxRetries: 3,
        delayInSec: 2,
        minErrorCode: 500
    );

$result = $factory->make();

// This will auto-retry on 500/502/503 errors
$response = $result->client->get('/unstable-api');

Random Desktop User-Agent Middleware ⭐ NEW

use JOOservices\Client\Factory\Factory;

// Generate a realistic desktop UA per request, reused on retries
$factory = (new Factory())
    ->enableRandomUserAgent(); // enabled by default, but you can inject a custom session here

$result = $factory->make();
$response = $result->client->get('https://api.example.com/profile');

// Each request receives a different desktop UA unless you reuse the same request ID/session.

Cache Middleware

// Provide your own cache middleware
$cacheMiddleware = function (callable $handler) {
    return function ($request, $options) use ($handler) {
        $cacheKey = md5((string)$request->getUri());

        if ($cached = cache()->get($cacheKey)) {
            return \GuzzleHttp\Promise\Create::promiseFor(
                new \GuzzleHttp\Psr7\Response(200, [], $cached)
            );
        }

        return $handler($request, $options)->then(
            function ($response) use ($cacheKey) {
                cache()->put($cacheKey, (string)$response->getBody(), 3600);
                return $response;
            }
        );
    };
};

$factory = (new Factory())->enableCache($cacheMiddleware);

Testing with Mocks

use GuzzleHttp\Psr7\Response;

$factory = (new Factory())
    ->fakeResponses([
        new Response(200, [], '{"id": 1}'),
        new Response(201, [], '{"created": true}'),
    ]);

$result = $factory->make();

// First request returns first response
$response1 = $result->client->get('/test1');
// Second request returns second response
$response2 = $result->client->post('/test2');

// Inspect request history
$history = $result->factory->getHistory($result->client);
$this->assertCount(2, $history);

Advanced Usage

Custom Request/Response Extractor

use JOOservices\Client\Logging\Contracts\RequestResponseExtractorInterface;

class MyCustomExtractor implements RequestResponseExtractorInterface
{
    public function extractRequestData($request, array $row): array
    {
        $row['custom_field'] = 'my_value';
        // ... extract custom data
        return $row;
    }

    public function extractResponseData($response, array $row): array
    {
        // ... extract custom data
        return $row;
    }
}

// Register in service provider
$this->app->bind(
    RequestResponseExtractorInterface::class,
    MyCustomExtractor::class
);

Querying Logs (MySQL)

use JOOservices\Client\Models\ClientRequestLog;

// Get all failed requests
$failed = ClientRequestLog::where('response_status', '>=', 400)->get();

// Get requests to specific API
$apiCalls = ClientRequestLog::where('path', 'like', '%/api/users%')
    ->whereDate('created_at', today())
    ->get();

// Get with error context
$errors = ClientRequestLog::where('level', 'error')
    ->get()
    ->each(function ($log) {
        $context = $log->context; // Auto-cast to array
        dump($context['error'] ?? null);
    });

Querying Logs (MongoDB)

use MongoDB\Client;

$client = new Client('mongodb://127.0.0.1:27017');
$collection = $client->selectDatabase('jooclient')->selectCollection('client_request_logs');

// Get all 500 errors from last hour
$errors = $collection->find([
    'response_status' => 500,
    'created_at' => ['$gte' => new \MongoDB\BSON\UTCDateTime(strtotime('-1 hour') * 1000)]
]);

foreach ($errors as $error) {
    echo $error['message'] . PHP_EOL;
}

Exception Handling

The package automatically logs all Guzzle exceptions with full context:

use GuzzleHttp\Exception\RequestException;

// Configure logging via .env
$factory = (new Factory())->enableLogging();
$result = $factory->make();

try {
    $response = $result->client->get('/api/endpoint');
} catch (RequestException $e) {
    // Exception is automatically logged with:
    // - Stack trace
    // - Error message
    // - Request details
    // - Response (if available)

    // Handle the error
    Log::error('API call failed', [
        'endpoint' => '/api/endpoint',
        'error' => $e->getMessage()
    ]);
}

Supported Guzzle Exceptions

All tested and logged automatically:

  • ConnectException - Connection failures
  • RequestException - General request failures
  • ServerException - 5xx errors
  • ClientException - 4xx errors
  • TooManyRedirectsException - Redirect loops

Performance

Batch Mode

For high-traffic applications, enable batch mode to reduce database writes:

// config/jooclient.php
'logging' => [
    'connection' => [
        'mysql' => [
            'batch' => true, // Buffer logs in memory
        ],
    ],
],

Important: Always flush at the end of your request:

$result = $factory->make();
// ... make many requests ...
$result->flushLogger(); // Flush buffered logs

Optimization Features

  • Schema Caching: Column list cached to avoid repeated queries
  • Batch Writes: Up to 500 inserts per transaction
  • Retry Logic: Smart retry for transient errors (connection, timeout, deadlock)
  • Size Limits: Auto-truncate large bodies to prevent memory issues
  • Connection Pooling: Reuses Laravel's database connections

Architecture

See ARCHITECTURE.md for detailed architecture documentation including:

  • Design patterns used
  • SOLID principles adherence
  • Data flow diagrams
  • Extension points
  • Performance optimizations

Testing

Run the full test suite:

vendor/bin/phpunit --testdox

Heads up: Integration suites for Redis and MySQL require those services to be running locally. Without them the cache and DB logging tests will fail.

Test Coverage

  • 195 tests, 547 assertions
  • MySQL logging (7 tests)
  • MongoDB logging (3 tests)
  • Guzzle exceptions (8 tests)
  • Middleware (cache, retry, history)
  • Edge cases (circular references, size limits, retry logic)

API Reference

Factory Methods

addOptions(array $options): self

Add Guzzle client options (immutable).

$factory = $factory->addOptions([
    'timeout' => 30,
    'connect_timeout' => 5,
    'base_uri' => 'https://api.example.com',
    'headers' => ['Authorization' => 'Bearer token'],
]);

enableRetries(int $maxRetries, int $delayInSec, int $minErrorCode): self

Enable automatic retry on server errors.

$factory = $factory->enableRetries(
    maxRetries: 3,
    delayInSec: 2,
    minErrorCode: 500
);

enableRandomUserAgent(?DesktopUserAgentSession $session = null): self

Enable or customise the desktop user-agent middleware that powers random rotation.

use JOOservices\Client\Services\DesktopUserAgentSession;
use JOOservices\Client\Generators\UserAgentGeneratorFactory;

$session = new DesktopUserAgentSession(UserAgentGeneratorFactory::createDefault());

$factory = (new Factory())
    ->enableRandomUserAgent($session);

enableLogging(array|null $config = null): self

Enable logging based on configuration. Automatically loads from config/jooclient.php and .env.

Supports:

  • MySQL logging (driver: 'mysql')
  • MongoDB logging (driver: 'mongodb')
  • Monolog logging (driver: 'monolog')
  • Multi-logger (driver: 'multi') - log to multiple destinations
// Uses config from .env and config/jooclient.php
$factory = (new Factory())->enableLogging();

// Or pass custom config array
$factory = (new Factory())->enableLogging([
    'logging' => [
        'enabled' => true,
        'driver' => 'mysql',  // or 'mongodb', 'monolog', 'multi'
        'connection' => [
            'mysql' => [
                'enabled' => true,
                'host' => '127.0.0.1',
                'port' => 3306,
                'database' => 'jooclient',
                'username' => 'root',
                'password' => 'root',
                'table' => 'client_request_logs',
                'batch' => false,
                'fallback' => 'error_log',
            ]
        ]
    ]
]);

enableCache(callable $cacheMiddleware): self

Enable cache middleware.

addMiddleware(callable $middleware, string $name): self

Add custom Guzzle middleware.

fakeResponses(array $responses): self

Mock responses for testing.

$factory = $factory->fakeResponses([
    new Response(200, [], 'OK'),
    new \GuzzleHttp\Exception\RequestException(
        'Error',
        new Request('GET', '/'),
        new Response(500)
    ),
]);

make(): Client

Create the configured Guzzle client.

Returns Client wrapper with:

  • client: Configured Guzzle client
  • factory: Factory instance (for history access)
  • logger: Logger instance (for manual flushing)

Client Methods

getLogger(): ?LoggerInterface

Get the logger instance.

flushLogger(): void

Flush buffered logs (important for batch mode).

Error Handling

Fallback Strategies

Configure what happens when logging fails:

'fallback' => 'error_log',  // Log to PHP error_log (default)
'fallback' => 'throw',       // Throw exception (fail fast)
'fallback' => 'silent',      // Suppress errors (not recommended)

Failure Logs

All logging failures are written to:

  • MySQL: /tmp/jooclient_db_logger_failures.log
  • MongoDB: /tmp/jooclient_mongodb_logger_failures.log

Each entry includes:

  • Timestamp
  • Exception message and class
  • Stack trace
  • Failed log data

Best Practices

1. Always Use Timeout

$factory = $factory->addOptions([
    'timeout' => 30,          // Total request timeout
    'connect_timeout' => 5,    // Connection timeout
]);

2. Enable Retries for External APIs

$factory = $factory->enableRetries(3, 2, 500);

3. Use Batch Mode for High Traffic

// config/jooclient.php
'logging' => [
    'connection' => [
        'mysql' => ['batch' => true],
    ],
],

// In your code
$result = $factory->make();
// ... many requests ...
$result->flushLogger(); // Don't forget!

4. Handle Exceptions Gracefully

use GuzzleHttp\Exception\GuzzleException;

try {
    $response = $result->client->get('/api/endpoint');
} catch (GuzzleException $e) {
    // All exceptions are logged automatically
    // Handle according to your needs
    return response()->json(['error' => 'Service unavailable'], 503);
}

5. Monitor Logs

// Set up alerts for frequent errors
$errorCount = ClientRequestLog::where('level', 'error')
    ->whereDate('created_at', today())
    ->count();

if ($errorCount > 100) {
    // Alert your team
}

Coding Standards

This project uses Laravel Pint with a strict PSR-12 baseline and Laravel-specific overrides for stylistic consistency (for example, Pint keeps the space after the ! operator that Laravel expects while PSR-12 would normally remove it). Static analysis and mess detection are handled by PHPStan and PHPMD respectively.

  • composer lint  — format the codebase in place (Laravel + PSR-12 aware).
  • composer lint:test  — validate formatting without modifying files (used in CI).
  • composer lint:phpcs  — run PHP_CodeSniffer (PSR-12 base with Laravel deviations).
  • composer lint:phpcs:fix  — auto-fix sniffs where possible.
  • composer analyse:phpstan  — run PHPStan (level 8) with Laravel helper allowances.
  • composer analyse:phpmd  — run PHPMD using the project ruleset tuned for Laravel naming.
  • composer analyse  — run both analysis toolchains sequentially.

The Pint configuration lives in pint.json so editors and CI stay aligned.

Security

Sensitive Data

Be cautious logging request/response bodies that may contain:

  • Passwords
  • API keys
  • Personal information
  • Credit card data

Consider implementing a custom extractor to filter sensitive fields:

class SecureExtractor extends RequestResponseExtractor
{
    public function extractRequestData($request, array $row): array
    {
        $row = parent::extractRequestData($request, $row);

        // Redact sensitive headers
        if (isset($row['request_headers'])) {
            $headers = json_decode($row['request_headers'], true);
            unset($headers['Authorization']);
            $row['request_headers'] = json_encode($headers);
        }

        return $row;
    }
}

Troubleshooting

Logs Not Appearing

  1. Check database connection
  2. Verify table exists (php artisan migrate)
  3. Check fallback logs: /tmp/jooclient_db_logger_failures.log
  4. Verify logging is enabled in config
  5. Call $result->flushLogger() if batch mode is enabled

Memory Issues

If experiencing memory issues with large responses:

// Bodies are automatically truncated to 65KB
// Adjust in RequestResponseExtractor if needed

Connection Pool Exhaustion

// Enable batch mode to reduce writes
'batch' => true,

// Increase max batch size if needed
// Default: 500 per transaction

Contributing

See architecture documentation and ensure:

  • All classes follow SOLID principles
  • All new features have tests
  • Documentation is updated
  • Code is PSR-12 compliant

Testing

# Run all tests
vendor/bin/phpunit --testdox

# Run specific test suite
vendor/bin/phpunit --filter GuzzleExceptions

# With coverage (requires xdebug)
vendor/bin/phpunit --coverage-html coverage/

See Testing Guide for comprehensive testing documentation.

📖 Additional Documentation

Implementation Details

License

Proprietary

Credits

  • Author: Viet Vu
  • Package: jooservices/jooclient
  • Version: 1.0.1

Support

For issues, questions, or feature requests, please contact the development team or open an issue in the repository.

Built with Laravel 12 and PHP 8.4 | Production Ready | Enterprise Grade