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
Requires
- php: ^8.4
- ext-pdo: *
- guzzlehttp/guzzle: ^7.0
- illuminate/database: ^12.0
- illuminate/support: ^12.0
- jooservices/jooagent: ^1.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- symfony/css-selector: ^7.3
- symfony/dom-crawler: ^7.0
Requires (Dev)
- laravel/pint: ^1.17
- mongodb/mongodb: ^2.0
- monolog/monolog: ^3.0
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^1.11
- phpunit/phpunit: 12.*
- squizlabs/php_codesniffer: ^3.10
Suggests
- ext-mongodb: Required for MongoDB logging driver
- ext-redis: Required for Redis cache driver
This package is auto-updated.
Last update: 2025-11-08 08:32:04 UTC
README
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 withisSuccess()andgetContent() - ✅ 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 customDesktopUserAgentSessionfor fine-grained control.- Documentation & Tests – Added coverage and usage notes for the user-agent middleware.
🚀 What's New in 1.1.0
Client::request()andClient::get()now return aResponseWrapperwith convenient helpers.- New
ResponseWrapper::isSuccess()andResponseWrapper::getContent()eliminate boilerplate status checks and body handling. ResponseWrapper::getContent()auto-converts JSON responses to arrays, wraps HTML in aDomWrapper(Symfony DomCrawler) and returnsnullfor non-2xx responses.- Documentation updated with new usage patterns and refreshed test statistics.
📚 Documentation
Getting Started
- Quick Examples - Jump right in with code
- Code Examples ⭐ NEW - 6 runnable examples
- Auto-Detection Guide - Intelligent driver auto-detection
Features & Guides
- Architecture - System design and SOLID principles
- Usage Guide - Comprehensive usage examples
- Multi-Logger Guide - Logging to multiple destinations
- Monolog Guide - File-based logging with rotation
- MongoDB Configuration - MongoDB setup and rotation
- Cache Guide - Redis & Filesystem caching
Operations & Monitoring
- Health Checks ⭐ NEW - Service availability monitoring
- Metrics Guide ⭐ NEW - Performance tracking
- Testing Guide - Testing and debugging
Reference
- Migration Guide - Upgrading from old versions
- Class Reference - API documentation
- Changelog ⭐ NEW - Version history
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-Agentheader 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 failuresRequestException- General request failuresServerException- 5xx errorsClientException- 4xx errorsTooManyRedirectsException- 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 clientfactory: 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
- Check database connection
- Verify table exists (
php artisan migrate) - Check fallback logs:
/tmp/jooclient_db_logger_failures.log - Verify logging is enabled in config
- 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
- Auto-Detection Implementation - Technical implementation details
- Cache Implementation - Cache system architecture
- SOLID Refactoring - SOLID principles applied
- Improvements Summary ⭐ NEW - Latest enhancements
- Feature Summary - Complete feature list
- Optimization Summary - Performance optimizations
- Cleanup Report - Code cleanup documentation
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