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