> /dev/null 2>&1 # hatif-missed-calls-alert-company-1 $newLine = "{$expr} {$phpBin} {$artisan} missed-calls:alert --company-id={$companyId} >> /dev/null 2>&1 {$marker}"; try { $result = self::upsertLine($marker, $newLine); if ($result) { Log::info("CronJobService: upserted missed-calls alert cron for company {$companyId} every {$minutes} min."); } else { Log::warning("CronJobService: crontab install returned non-zero for company {$companyId}."); } return $result; } catch (\Throwable $e) { Log::error('CronJobService: upsert failed', [ 'company_id' => $companyId, 'error' => $e->getMessage(), ]); return false; } } /** * Remove the missed-calls alert cron job for a company. * * @param int $companyId * @return bool */ public static function removeMissedCallsAlert(int $companyId): bool { $marker = self::marker($companyId); try { $result = self::removeLine($marker); if ($result) { Log::info("CronJobService: removed missed-calls alert cron for company {$companyId}."); } return $result; } catch (\Throwable $e) { Log::error('CronJobService: remove failed', [ 'company_id' => $companyId, 'error' => $e->getMessage(), ]); return false; } } /** * Check whether a cron entry exists for the given company. */ public static function exists(int $companyId): bool { $marker = self::marker($companyId); $lines = self::readCrontab(); foreach ($lines as $line) { if (str_contains($line, $marker)) { return true; } } return false; } // ───────────────────────────────────────────────────────────── // Internal helpers // ───────────────────────────────────────────────────────────── private static function marker(int $companyId): string { return self::MARKER_PREFIX . $companyId; } /** * Build a valid cron expression for "every N minutes". * Minutes must be a divisor-friendly value; for values that don't divide * evenly into 60 we fall back to the slash-step cron syntax, which is * universally supported. */ private static function cronExpression(int $minutes): string { if ($minutes === 1) { return '* * * * *'; } return "*/{$minutes} * * * *"; } /** Read the current crontab lines for the running user */ private static function readCrontab(): array { exec('crontab -l 2>/dev/null', $lines); return is_array($lines) ? $lines : []; } /** Install an array of lines as the new crontab */ private static function installCrontab(array $lines): bool { // Ensure file ends with a newline — required by POSIX $content = implode("\n", $lines); if ($content !== '' && substr($content, -1) !== "\n") { $content .= "\n"; } $tmp = tempnam(sys_get_temp_dir(), 'hatif_cron_'); file_put_contents($tmp, $content); exec("crontab {$tmp}", $output, $exitCode); @unlink($tmp); return $exitCode === 0; } private static function upsertLine(string $marker, string $newLine): bool { // Remove any existing entry with this marker, then append the new one $lines = self::readCrontab(); $filtered = array_values( array_filter($lines, fn ($l) => !str_contains($l, $marker)) ); $filtered[] = $newLine; return self::installCrontab($filtered); } private static function removeLine(string $marker): bool { $lines = self::readCrontab(); $filtered = array_values( array_filter($lines, fn ($l) => !str_contains($l, $marker)) ); // Nothing changed — treat as success if (count($filtered) === count($lines)) { return true; } return self::installCrontab($filtered); } }