first commit
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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',
|
||||
];
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 ?? [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user