Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Rector as dev dependency for automated code refactoring
- Additional PHP extensions required in CI: `bcmath`, `gd`, `zip`
- PHPUnit strict testing flags: `--fail-on-warning`, `--fail-on-risky`
- **Cron Scheduler**: New CLI command `php qt cron:run` for running scheduled tasks
- Task definition via PHP files in `cron/` directory
- Cron expression parsing using `dragonmantank/cron-expression` library
- File-based task locking to prevent concurrent execution
- Comprehensive logging of task execution and errors
- Support for force mode and specific task execution
- Automatic cleanup of stale locks (older than 24 hours)
- Full documentation in `docs/cron-scheduler.md`

### Removed
- Support for PHP 7.3 and earlier versions
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"league/commonmark": "^2.0",
"ezyang/htmlpurifier": "^4.18",
"povils/figlet": "^0.1.0",
"ramsey/uuid": "^4.2"
"ramsey/uuid": "^4.2",
"dragonmantank/cron-expression": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
Expand Down
140 changes: 140 additions & 0 deletions src/Console/Commands/CronRunCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

/**
* Quantum PHP Framework
*
* An open source software development framework for PHP
*
* @package Quantum
* @author Arman Ag. <arman.ag@softberg.org>
* @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
* @link http://quantum.softberg.org/
* @since 3.0.0
*/

namespace Quantum\Console\Commands;

use Quantum\Libraries\Cron\Exceptions\CronException;
use Quantum\Libraries\Cron\CronManager;
use Quantum\Console\QtCommand;

/**
* Class CronRunCommand
* @package Quantum\Console
*/
class CronRunCommand extends QtCommand
{
/**
* The console command name.
* @var string
*/
protected $name = 'cron:run';

/**
* The console command description.
* @var string
*/
protected $description = 'Run scheduled cron tasks';

/**
* Command help text.
* @var string
*/
protected $help = 'Executes scheduled tasks defined in the cron directory. Use --task to run a single task or --force to bypass locks.';

/**
* Command options
* @var array<int, list<string|null>>
*/
protected $options = [
['force', 'f', 'none', 'Force run tasks ignoring locks'],
['task', 't', 'optional', 'Run a specific task by name'],
['path', 'p', 'optional', 'Custom cron directory path'],
];

/**
* Executes the command
*/
public function exec()
{
$force = (bool) $this->getOption('force');
$taskName = $this->getOption('task');
$cronPath = $this->getOption('path') ?: cron_config('path');

try {
$manager = new CronManager($cronPath);

if ($taskName) {
$this->runSpecificTask($manager, $taskName, $force);
} else {
$this->runAllDueTasks($manager, $force);
}
} catch (CronException $e) {
$this->error($e->getMessage());
} catch (\Throwable $e) {
$this->error('Unexpected error: ' . $e->getMessage());
}
}

/**
* Run all due tasks
* @param CronManager $manager
* @param bool $force
* @return void
*/
private function runAllDueTasks(CronManager $manager, bool $force): void
{
$this->info('Running scheduled tasks...');

$stats = $manager->runDueTasks($force);

$this->output('');
$this->info('Execution Summary:');
$this->output(" Total tasks: {$stats['total']}");
$this->output(" Executed: <info>{$stats['executed']}</info>");
$this->output(" Skipped: {$stats['skipped']}");

if ($stats['locked'] > 0) {
$this->output(" Locked: <comment>{$stats['locked']}</comment>");
}

if ($stats['failed'] > 0) {
$this->output(" Failed: <error>{$stats['failed']}</error>");
}

$this->output('');

if ($stats['executed'] > 0) {
$this->info('✓ Tasks completed successfully');
} elseif ($stats['total'] === 0) {
$this->comment('No tasks found in cron directory');
} else {
$this->comment('No tasks were due to run');
}
}

/**
* Run a specific task
* @param CronManager $manager
* @param string $taskName
* @param bool $force
* @return void
* @throws CronException
*/
private function runSpecificTask(CronManager $manager, string $taskName, bool $force): void
{
$this->info("Running task: {$taskName}");

$manager->runTaskByName($taskName, $force);

$stats = $manager->getStats();

if ($stats['executed'] > 0) {
$this->info("✓ Task '{$taskName}' completed successfully");
} elseif ($stats['failed'] > 0) {
$this->error("✗ Task '{$taskName}' failed");
} elseif ($stats['locked'] > 0) {
$this->comment("⚠ Task '{$taskName}' is locked");
}
}
}
46 changes: 46 additions & 0 deletions src/Libraries/Cron/Contracts/CronTaskInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* Quantum PHP Framework
*
* An open source software development framework for PHP
*
* @package Quantum
* @author Arman Ag. <arman.ag@softberg.org>
* @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
* @link http://quantum.softberg.org/
* @since 3.0.0
*/

namespace Quantum\Libraries\Cron\Contracts;

/**
* Interface CronTaskInterface
* @package Quantum\Libraries\Cron
*/
interface CronTaskInterface
{
/**
* Get the cron expression
* @return string
*/
public function getExpression(): string;

/**
* Get the task name
* @return string
*/
public function getName(): string;

/**
* Check if the task should run at the current time
* @return bool
*/
public function shouldRun(): bool;

/**
* Execute the task
* @return void
*/
public function handle(): void;
}
Loading