first commit

This commit is contained in:
2026-03-22 22:06:59 +00:00
commit ad5b3fde24
12 changed files with 1081 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
# Zoho OAuth2 Credentials
# Get these from your Zoho API Console: https://api-console.zoho.com/
ZOHO_CLIENT_ID=your_client_id_here
ZOHO_CLIENT_SECRET=your_client_secret_here
ZOHO_REDIRECT_URI=http://localhost/callback
+137
View File
@@ -0,0 +1,137 @@
# Zoho API Integration
Modular PHP integration with Zoho APIs (Projects, Desk, and more).
## Features
- **Modular Architecture**: Easy to add new Zoho services
- **OAuth2 Authentication**: Secure token management with auto-refresh
- **Multiple Export Formats**: JSON and CSV support
- **Logging**: File-based logging for monitoring
- **Cron-Ready**: Built for hourly automated exports
## Directory Structure
```
zoho-qwen/
├── config/
│ └── config.php # Configuration settings
├── src/
│ ├── Core/
│ │ └── ZohoClient.php # OAuth2 & HTTP client
│ ├── Interfaces/
│ │ └── ZohoServiceInterface.php
│ ├── Services/
│ │ ├── ProjectsService.php
│ │ └── DeskService.php
│ ├── Export/
│ │ └── ExportManager.php
│ └── Utils/
│ └── Logger.php
├── scripts/
│ ├── authenticate.php # OAuth2 setup
│ └── export_projects.php # Export script
├── cron/
│ └── hourly_export.sh # Cron wrapper
└── storage/ # Tokens, exports, logs (auto-created)
```
## Setup
### 1. Install Requirements
- PHP 8.0+
- cURL extension
### 2. Configure Environment
```bash
cp .env.example .env
# Edit .env with your Zoho credentials
```
### 3. Register Zoho Application
1. Visit [Zoho API Console](https://api-console.zoho.com/)
2. Create a new client
3. Select "Server-based Application"
4. Add your redirect URI
5. Copy Client ID and Client Secret to `.env`
### 4. Authenticate
```bash
# Generate authorization URL
php scripts/authenticate.php --service=projects
# Visit the URL, authorize, then exchange the code:
php scripts/authenticate.php --service=projects --code=YOUR_CODE
```
### 5. Run Export
```bash
# Manual export
php scripts/export_projects.php
# Include task details
php scripts/export_projects.php --include-tasks
# Export as CSV
php scripts/export_projects.php --format=csv
```
### 6. Setup Cron (Optional)
```bash
# Edit the cron script path
nano cron/hourly_export.sh
# Add to crontab (runs every hour)
crontab -e
# Add: 0 * * * * /path/to/cron/hourly_export.sh
```
## Adding New Zoho Services
1. Add service config in `config/config.php`:
```php
'desk' => [
'enabled' => true,
'api_base' => 'https://desk.zoho.com/api/v1',
'scopes' => 'ZohoDesk.tickets.READ',
],
```
2. Create service class implementing `ZohoServiceInterface`:
```php
class DeskService implements ZohoServiceInterface
{
// Implement required methods
}
```
3. Create export script for the service
## Configuration
Edit `config/config.php` to customize:
- API domains (for EU/US data centers)
- OAuth scopes
- Export formats and locations
- Log levels
## Troubleshooting
**Token expired errors**: Re-run authentication script
**401 Unauthorized**: Check OAuth scopes in config
**Export directory not writable**: `chmod 755 storage/exports`
## License
MIT
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* Configuration settings for Zoho API connections
*/
return [
'zoho' => [
'accounts_domain' => 'https://accounts.zoho.com',
'api_domain' => 'https://www.zohoapis.com',
// OAuth2 credentials
'client_id' => getenv('ZOHO_CLIENT_ID') ?: '',
'client_secret' => getenv('ZOHO_CLIENT_SECRET') ?: '',
'redirect_uri' => getenv('ZOHO_REDIRECT_URI') ?: '',
// Token storage
'token_file' => __DIR__ . '/../storage/tokens.json',
// Services configuration
'projects' => [
'enabled' => true,
'api_base' => 'https://www.zohoapis.com/projects/api/v3',
'scopes' => 'ZohoProjects.projects.READ,ZohoProjects.tasks.READ',
],
'desk' => [
'enabled' => false,
'api_base' => 'https://desk.zoho.com/api/v1',
'scopes' => 'ZohoDesk.tickets.READ,ZohoDesk.organizations.READ',
],
],
// Export settings
'export' => [
'output_dir' => __DIR__ . '/../storage/exports',
'format' => 'json', // json, csv
],
// Logging
'log_file' => __DIR__ . '/../storage/logs/zoho_sync.log',
'log_level' => 'INFO',
];
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
#
# Cron job script to export Zoho Projects hourly
# Add to crontab: 0 * * * * /path/to/cron/hourly_export.sh
#
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
PHP_BIN="${PHP_BIN:-php}"
# Change to project directory
cd "$PROJECT_ROOT"
# Log start
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting hourly Zoho Projects export"
# Run the export script
"$PHP_BIN" scripts/export_projects.php --include-tasks
# Log completion
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Export completed successfully"
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* Initial authentication script for Zoho OAuth2
*
* Step 1: Run this script to get the authorization URL
* Step 2: Visit the URL and authorize the application
* Step 3: Copy the authorization code from the redirect URL
* Step 4: Run: php scripts/authenticate.php --code=YOUR_CODE
*/
declare(strict_types=1);
// Autoloader
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
use App\Core\ZohoClient;
// Load configuration
$config = require __DIR__ . '/../config/config.php';
// Parse command line options
$options = getopt('', ['service:', 'code:', 'help']);
$service = $options['service'] ?? 'projects';
$help = isset($options['help']);
if ($help) {
echo "Zoho OAuth2 Authentication Script\n";
echo "=================================\n\n";
echo "Usage:\n";
echo " 1. Generate authorization URL:\n";
echo " php scripts/authenticate.php --service=projects\n\n";
echo " 2. Visit the URL, authorize, and copy the code from redirect URL\n\n";
echo " 3. Exchange code for tokens:\n";
echo " php scripts/authenticate.php --service=projects --code=YOUR_CODE\n\n";
echo "Options:\n";
echo " --service Zoho service (projects, desk)\n";
echo " --code Authorization code from OAuth flow\n";
echo " --help Show this help message\n";
exit(0);
}
$client = new ZohoClient($config);
if (isset($options['code'])) {
// Exchange authorization code for tokens
$code = $options['code'];
echo "Exchanging authorization code for tokens...\n";
try {
$client->exchangeCodeForTokens($code, $service);
echo "Success! Tokens have been saved.\n";
echo "You can now run the export script.\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
} else {
// Generate authorization URL
try {
$authUrl = $client->getAuthorizationUrl($service);
echo "Authorization URL for Zoho {$service}:\n";
echo "========================================\n";
echo "{$authUrl}\n\n";
echo "1. Visit this URL in your browser\n";
echo "2. Authorize the application\n";
echo "3. Copy the 'code' parameter from the redirect URL\n";
echo "4. Run: php scripts/authenticate.php --service={$service} --code=YOUR_CODE\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* Hourly script to export open projects from Zoho Projects
*
* Usage: php scripts/export_projects.php [--include-tasks] [--format=json|csv]
*/
declare(strict_types=1);
// Autoloader
spl_autoload_register(function ($class) {
$prefix = 'App\\';
$baseDir = __DIR__ . '/../src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relativeClass = substr($class, $len);
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
if (file_exists($file)) {
require $file;
}
});
use App\Core\ZohoClient;
use App\Services\ProjectsService;
use App\Export\ExportManager;
use App\Utils\Logger;
// Load configuration
$config = require __DIR__ . '/../config/config.php';
// Initialize logger
$logger = new Logger($config['log_file'], $config['log_level']);
try {
$logger->info('Starting Zoho Projects export');
// Parse command line options
$options = getopt('', ['include-tasks', 'format:']);
$includeTasks = isset($options['include-tasks']);
$format = $options['format'] ?? $config['export']['format'];
// Initialize Zoho client
$client = new ZohoClient($config);
// Initialize Projects service
$projectsService = new ProjectsService($client, $config);
// Fetch open projects
$logger->info('Fetching open projects', ['include_tasks' => $includeTasks]);
$projects = $projectsService->getOpenProjects($includeTasks);
$logger->info('Fetched projects', ['count' => count($projects)]);
// Export data
$exportManager = new ExportManager($config['export']['output_dir'], $format);
$filepath = $exportManager->export($projects, 'open_projects');
$logger->info('Export completed', ['filepath' => $filepath]);
echo "Export completed successfully!\n";
echo "Records exported: " . count($projects) . "\n";
echo "Output file: {$filepath}\n";
} catch (\Exception $e) {
$logger->error('Export failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}
+251
View File
@@ -0,0 +1,251 @@
<?php
namespace App\Core;
use App\Interfaces\ZohoServiceInterface;
/**
* Core Zoho API client handling OAuth2 authentication and HTTP requests
*/
class ZohoClient
{
private array $config;
private ?string $accessToken = null;
private ?string $refreshToken = null;
private ?int $tokenExpiry = null;
public function __construct(array $config)
{
$this->config = $config;
$this->loadTokens();
}
/**
* Load tokens from storage
*/
private function loadTokens(): void
{
$tokenFile = $this->config['zoho']['token_file'];
if (file_exists($tokenFile)) {
$tokens = json_decode(file_get_contents($tokenFile), true);
if ($tokens) {
$this->accessToken = $tokens['access_token'] ?? null;
$this->refreshToken = $tokens['refresh_token'] ?? null;
$this->tokenExpiry = $tokens['expires_at'] ?? null;
}
}
}
/**
* Save tokens to storage
*/
private function saveTokens(): void
{
$tokenFile = $this->config['zoho']['token_file'];
$dir = dirname($tokenFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$tokens = [
'access_token' => $this->accessToken,
'refresh_token' => $this->refreshToken,
'expires_at' => $this->tokenExpiry,
];
file_put_contents($tokenFile, json_encode($tokens, JSON_PRETTY_PRINT));
}
/**
* Check if access token is expired or about to expire
*/
private function isTokenExpired(): bool
{
if (!$this->tokenExpiry) {
return true;
}
// Refresh 5 minutes before actual expiry
return time() >= ($this->tokenExpiry - 300);
}
/**
* Get valid access token, refreshing if necessary
*/
public function getAccessToken(): string
{
if ($this->isTokenExpired()) {
$this->refreshAccessToken();
}
return $this->accessToken;
}
/**
* Initial OAuth2 authorization - generates authorization URL
*/
public function getAuthorizationUrl(string $service): string
{
$serviceConfig = $this->config['zoho'][$service] ?? null;
if (!$serviceConfig) {
throw new \InvalidArgumentException("Service '{$service}' not configured");
}
$params = [
'client_id' => $this->config['zoho']['client_id'],
'response_type' => 'code',
'redirect_uri' => $this->config['zoho']['redirect_uri'],
'scope' => $serviceConfig['scopes'],
'access_type' => 'offline',
'prompt' => 'consent',
];
return $this->config['zoho']['accounts_domain'] . '/oauth/v2/auth?' . http_build_query($params);
}
/**
* Exchange authorization code for tokens
*/
public function exchangeCodeForTokens(string $code, string $service): void
{
$serviceConfig = $this->config['zoho'][$service] ?? null;
if (!$serviceConfig) {
throw new \InvalidArgumentException("Service '{$service}' not configured");
}
$response = $this->makeRequest(
$this->config['zoho']['accounts_domain'] . '/oauth/v2/token',
'POST',
[
'client_id' => $this->config['zoho']['client_id'],
'client_secret' => $this->config['zoho']['client_secret'],
'code' => $code,
'redirect_uri' => $this->config['zoho']['redirect_uri'],
'grant_type' => 'authorization_code',
]
);
$this->storeTokens($response);
}
/**
* Refresh access token using refresh token
*/
private function refreshAccessToken(): void
{
if (!$this->refreshToken) {
throw new \RuntimeException('No refresh token available. Please re-authorize.');
}
$response = $this->makeRequest(
$this->config['zoho']['accounts_domain'] . '/oauth/v2/token',
'POST',
[
'client_id' => $this->config['zoho']['client_id'],
'client_secret' => $this->config['zoho']['client_secret'],
'refresh_token' => $this->refreshToken,
'grant_type' => 'refresh_token',
]
);
// Refresh token may be rotated, update if present
if (isset($response['refresh_token'])) {
$this->refreshToken = $response['refresh_token'];
}
$this->storeTokens($response, false);
}
/**
* Store tokens from API response
*/
private function storeTokens(array $response, bool $includeRefresh = true): void
{
$this->accessToken = $response['access_token'];
if ($includeRefresh && isset($response['refresh_token'])) {
$this->refreshToken = $response['refresh_token'];
}
$this->tokenExpiry = time() + ($response['expires_in'] ?? 3600);
$this->saveTokens();
}
/**
* Make authenticated API request to Zoho service
*/
public function request(ZohoServiceInterface $service, string $endpoint, string $method = 'GET', array $params = []): array
{
$url = rtrim($service->getApiBase(), '/') . '/' . ltrim($endpoint, '/');
$headers = [
'Authorization' => 'Zoho-oauthtoken ' . $this->getAccessToken(),
];
return $this->makeRequest($url, $method, $params, $headers);
}
/**
* Generic HTTP request handler
*/
private function makeRequest(string $url, string $method, array $params = [], array $headers = []): array
{
$ch = curl_init();
$headers = array_merge([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
], $headers);
$headerArray = [];
foreach ($headers as $key => $value) {
$headerArray[] = "{$key}: {$value}";
}
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headerArray,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30,
]);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
} elseif ($method === 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
} elseif ($method === 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
} elseif ($params && $method === 'GET') {
curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new \RuntimeException("cURL error: {$error}");
}
$data = json_decode($response, true);
if ($httpCode >= 400) {
throw new \RuntimeException(
"API request failed with status {$httpCode}: " .
($data['message'] ?? $data['error'] ?? 'Unknown error')
);
}
return $data ?? [];
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace App\Export;
/**
* Handles data export to various formats
*/
class ExportManager
{
private string $outputDir;
private string $format;
public function __construct(string $outputDir, string $format = 'json')
{
$this->outputDir = $outputDir;
$this->format = $format;
if (!is_dir($this->outputDir)) {
mkdir($this->outputDir, 0755, true);
}
}
/**
* Export data to file
*/
public function export(array $data, string $filename): string
{
$timestamp = date('Y-m-d_His');
$fullFilename = "{$filename}_{$timestamp}.{$this->format}";
$filepath = $this->outputDir . '/' . $fullFilename;
$content = match ($this->format) {
'csv' => $this->toCsv($data),
'json' => $this->toJson($data),
default => throw new \InvalidArgumentException("Unsupported format: {$this->format}"),
};
file_put_contents($filepath, $content);
return $filepath;
}
/**
* Convert data to JSON format
*/
private function toJson(array $data): string
{
return json_encode([
'exported_at' => date('c'),
'record_count' => count($data),
'data' => $data,
], JSON_PRETTY_PRINT);
}
/**
* Convert data to CSV format
*/
private function toCsv(array $data): string
{
if (empty($data)) {
return '';
}
$output = fopen('php://temp', 'r+');
// Flatten nested arrays for CSV
$flattened = array_map(fn($row) => $this->flattenArray($row), $data);
// Write headers
$headers = array_keys(reset($flattened));
fputcsv($output, $headers);
// Write data rows
foreach ($flattened as $row) {
fputcsv($output, array_values($row));
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
/**
* Flatten nested array for CSV export
*/
private function flattenArray(array $array, string $prefix = ''): array
{
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
// Check if it's a simple array (like assignees list)
if (array_values($value) === $value && !empty($value)) {
$result[$newKey] = implode('; ', $value);
} else {
$result = array_merge($result, $this->flattenArray($value, $newKey));
}
} else {
$result[$newKey] = $value;
}
}
return $result;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Interfaces;
/**
* Interface for Zoho service implementations
*/
interface ZohoServiceInterface
{
/**
* Get the service name identifier
*/
public function getName(): string;
/**
* Get the API scopes required for this service
*/
public function getScopes(): string;
/**
* Get the base API URL for this service
*/
public function getApiBase(): string;
/**
* Fetch data from the service
*/
public function fetchData(array $params = []): array;
/**
* Transform raw API data into exportable format
*/
public function transformData(array $data): array;
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Services;
use App\Core\ZohoClient;
use App\Interfaces\ZohoServiceInterface;
/**
* Zoho Desk service implementation
* Ready for future integration
*/
class DeskService implements ZohoServiceInterface
{
private ZohoClient $client;
private array $config;
public function __construct(ZohoClient $client, array $config)
{
$this->client = $client;
$this->config = $config['zoho']['desk'];
}
public function getName(): string
{
return 'desk';
}
public function getScopes(): string
{
return $this->config['scopes'];
}
public function getApiBase(): string
{
return $this->config['api_base'];
}
/**
* Fetch tickets from Zoho Desk
*/
public function fetchData(array $params = []): array
{
$defaultParams = [
'status' => 'Open',
'limit' => 200,
];
$params = array_merge($defaultParams, $params);
$response = $this->client->request($this, 'tickets', 'GET', $params);
return $response['data'] ?? [];
}
/**
* Transform ticket data for export
*/
public function transformData(array $data): array
{
return array_map(function ($ticket) {
return [
'ticket_id' => $ticket['id'] ?? null,
'subject' => $ticket['subject'] ?? null,
'description' => $ticket['description'] ?? null,
'status' => $ticket['status'] ?? null,
'priority' => $ticket['priority'] ?? null,
'created_time' => $ticket['createdTime'] ?? null,
'modified_time' => $ticket['modifiedTime'] ?? null,
'due_by' => $ticket['dueBy'] ?? null,
'contact' => [
'id' => $ticket['contactId'] ?? null,
'name' => $ticket['contactName'] ?? null,
'email' => $ticket['contactEmail'] ?? null,
],
'department' => $ticket['departmentId'] ?? null,
'assignee' => [
'id' => $ticket['assigneeId'] ?? null,
'name' => $ticket['assigneeName'] ?? null,
],
];
}, $data);
}
/**
* Get open tickets
*/
public function getOpenTickets(): array
{
$tickets = $this->fetchData();
return $this->transformData($tickets);
}
}
+141
View File
@@ -0,0 +1,141 @@
<?php
namespace App\Services;
use App\Core\ZohoClient;
use App\Interfaces\ZohoServiceInterface;
/**
* Zoho Projects service implementation
*/
class ProjectsService implements ZohoServiceInterface
{
private ZohoClient $client;
private array $config;
public function __construct(ZohoClient $client, array $config)
{
$this->client = $client;
$this->config = $config['zoho']['projects'];
}
public function getName(): string
{
return 'projects';
}
public function getScopes(): string
{
return $this->config['scopes'];
}
public function getApiBase(): string
{
return $this->config['api_base'];
}
/**
* Fetch all open projects from Zoho Projects
*/
public function fetchData(array $params = []): array
{
$defaultParams = [
'status' => '1', // 1 = Open/Active projects
'per_page' => 200,
];
$params = array_merge($defaultParams, $params);
$response = $this->client->request($this, 'projects', 'GET', $params);
return $response['data'] ?? [];
}
/**
* Fetch project tasks for a specific project
*/
public function fetchProjectTasks(string $projectId, array $params = []): array
{
$defaultParams = [
'per_page' => 200,
];
$params = array_merge($defaultParams, $params);
$endpoint = "projects/{$projectId}/tasks";
$response = $this->client->request($this, $endpoint, 'GET', $params);
return $response['data'] ?? [];
}
/**
* Transform project data for export
*/
public function transformData(array $data): array
{
return array_map(function ($project) {
return [
'project_id' => $project['project_id'] ?? null,
'project_name' => $project['name'] ?? null,
'description' => $project['description'] ?? null,
'status' => $project['status'] ?? null,
'start_date' => $project['start_date'] ?? null,
'end_date' => $project['end_date'] ?? null,
'created_date' => $project['created_date'] ?? null,
'owner' => [
'id' => $project['owner']['id'] ?? null,
'name' => $project['owner']['name'] ?? null,
],
'customer' => [
'id' => $project['customer']['id'] ?? null,
'name' => $project['customer']['name'] ?? null,
],
'budget' => $project['budget'] ?? null,
'currency' => $project['currency_code'] ?? null,
'task_count' => $project['task_count']['total'] ?? null,
'task_progress' => [
'completed' => $project['task_count']['completed'] ?? 0,
'total' => $project['task_count']['total'] ?? 0,
],
];
}, $data);
}
/**
* Get open projects with optional task details
*/
public function getOpenProjects(bool $includeTasks = false): array
{
$projects = $this->fetchData();
$transformed = $this->transformData($projects);
if ($includeTasks) {
foreach ($transformed as &$project) {
$tasks = $this->fetchProjectTasks($project['project_id']);
$project['tasks'] = $this->transformTasks($tasks);
}
}
return $transformed;
}
/**
* Transform task data
*/
private function transformTasks(array $tasks): array
{
return array_map(function ($task) {
return [
'task_id' => $task['task_id'] ?? null,
'task_name' => $task['name'] ?? null,
'status' => $task['status'] ?? null,
'priority' => $task['priority'] ?? null,
'assignees' => array_map(
fn($a) => $a['name'] ?? null,
$task['assignees'] ?? []
),
'due_date' => $task['due_date'] ?? null,
];
}, $tasks);
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Utils;
/**
* Simple file-based logger
*/
class Logger
{
public const LEVELS = [
'DEBUG' => 0,
'INFO' => 1,
'WARNING' => 2,
'ERROR' => 3,
'CRITICAL' => 4,
];
private string $logFile;
private int $minLevel;
public function __construct(string $logFile, string $minLevel = 'INFO')
{
$this->logFile = $logFile;
$this->minLevel = self::LEVELS[$minLevel] ?? self::LEVELS['INFO'];
$dir = dirname($this->logFile);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
public function debug(string $message, array $context = []): void
{
$this->log('DEBUG', $message, $context);
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
public function critical(string $message, array $context = []): void
{
$this->log('CRITICAL', $message, $context);
}
private function log(string $level, string $message, array $context = []): void
{
if (self::LEVELS[$level] < $this->minLevel) {
return;
}
$timestamp = date('Y-m-d H:i:s');
$formattedMessage = $this->interpolate($message, $context);
$logLine = "[{$timestamp}] [{$level}] {$formattedMessage}" . PHP_EOL;
file_put_contents($this->logFile, $logLine, FILE_APPEND);
}
private function interpolate(string $message, array $context = []): string
{
$replacements = [];
foreach ($context as $key => $value) {
$replacements['{' . $key . '}'] = is_scalar($value) ? $value : json_encode($value);
}
return strtr($message, $replacements);
}
}