Cron jobs and CLI commands
Run scheduled work from a FeatherPanel plugin with Cron classes and the Cron helper, and add your own commands to the panel CLI with CommandBuilder.
On this page
Cron jobs
How the runner calls your class
The PHP cron runner, backend/storage/cron/runner.php, starts once a minute (see
Cron and workers for how it is scheduled). On each run it:
- runs the core jobs in
storage/cron/php/, - for every folder in
storage/addons/, loads eachCron/*.phpfile and instantiates the classApp\Addons\<folder>\Cron\<file name>, - calls
run()on it.
Things to know:
- The runner does not check whether the plugin is valid or loaded. Any folder with a
Cron/directory is run. - If the class name does not match, the output shows
Class App\Addons\... not foundand the file is skipped. - The runner catches
Exceptiononly. A PHPError, such as aTypeErroror a call to an undefined method, stops the whole run, so jobs after yours do not run that minute. - Output goes to the cron log,
/var/log/cron.loginside the Docker backend container. php cli cronruns the core jobs only. To test a plugin job, run the runner itself from the backend folder:php storage/cron/runner.php.
A cron class
Implement App\Cron\TimeTask and use App\Cron\Cron to decide whether the work is due:
<?php
namespace App\Addons\myplugin\Cron;
use App\App;
use App\Chat\TimedTask;
use App\Cron\Cron;
use App\Cron\TimeTask;
class Cleanup implements TimeTask
{
public function run()
{
$cron = new Cron('myplugin-cleanup', '1H');
try {
$cron->runIfDue(function (): void {
// Delete old rows, call an API, send a report...
TimedTask::markRun('myplugin-cleanup', true, 'cleanup finished');
});
} catch (\Throwable $e) {
App::getInstance(false, true)->getLogger()->error('myplugin cleanup failed: ' . $e->getMessage());
TimedTask::markRun('myplugin-cleanup', false, $e->getMessage());
}
}
}
The file is Cron/Cleanup.php. The namespace must be App\Addons\<identifier>\Cron and the class name must equal
the file name.
The Cron helper
new Cron(string $identifier, string $interval):
| Part | Notes |
|---|---|
$identifier | Names the state file, storage/caches/cron/<identifier>.fptj, which holds the last run time. Prefix it with your plugin identifier so it cannot clash with core jobs or other plugins. |
$interval | A whole number followed by an upper-case unit: S seconds, M minutes, H hours, D days, W weeks. Examples: 5M, 1H, 1D. Anything else, such as 1m or 1.5H, throws an exception when the Cron object is created. See the note below for S, MO and Y. |
| Method | Does |
|---|---|
runIfDue(callable $callback, bool $force = false): bool | Runs the callback when the interval has passed since the last successful run, or when $force is true. If the callback throws an Exception, the run is logged as Cron job <id> failed and not marked, so it is tried again next minute. |
shouldRun(): bool | Whether the interval has passed. |
markAsRun(): void | Stores the current time as the last run. |
getLastRunTime(): ?DateTime, getNextRunTime(): DateTime | Read the schedule. |
Delete the .fptj file to make the job run on the next minute.
App\Chat\TimedTask::markRun(string $name, bool $success, ?string $message) records the result in the panel’s timed
task table, the same way the core jobs do. It is optional.
CLI commands
How commands are found
The panel CLI is the file backend/cli, run as php cli <command> from the backend folder. The installer also adds a
featherpanel command that runs php cli inside the Docker backend container. See Panel CLI.
When you run a command, the CLI first looks for a plugin command:
- On v1.5.2 and earlier it scans
<current directory>/backend/storage/addons/*/Commands/*.php. From v1.5.3 (PR #365) it scans the panel’s addons directory (APP_ADDONS_DIR,backend/storage/addons) wherever you run it from, and still checks the old location as a fallback. - For each file it loads the class
App\Addons\<folder>\Commands\<file name>. - The first class whose name matches the command, ignoring case, runs. Plugin commands are checked before built-in
ones, so a plugin command named
migratewould replace the core command. Pick a name that cannot clash.
help lists plugin commands under Plugins: using the same lookup.
The CLI only runs from /var/www/featherpanel, /var/www/featherpanel/backend or /var/www/html unless you pass
--skip-path-check. On v1.5.2 and earlier, because of the <current directory>/backend/storage/addons lookup,
plugin commands are only found when the current directory is the repository root:
cd /var/www/featherpanel
php backend/cli myplugincleanup
In the Docker image the working directory is /var/www/html, which is the backend folder itself, so on v1.5.2 and
earlier php cli and the featherpanel wrapper do not find plugin commands there. Use a cron job or an admin route
for work that has to run in every setup. Discovery from any directory, including Docker, is fixed in v1.5.3 (PR #365).
A command class
<?php
namespace App\Addons\myplugin\Commands;
use App\Cli\App;
use App\Cli\CommandBuilder;
class MyPluginCleanup implements CommandBuilder
{
public static function execute(array $args): void
{
$cli = App::getInstance();
$cli->send('&aCleanup started');
// $args[0] is the command name itself; your arguments start at $args[1].
$days = (int) ($args[1] ?? 30);
$cli->send('&7Removing entries older than &f' . $days . ' &7days');
}
public static function getDescription(): string
{
return 'Remove old MyPlugin entries';
}
public static function getSubCommands(): array
{
return [];
}
}
The file is Commands/MyPluginCleanup.php and the command is myplugincleanup. The Create file tool names the
class <Name>Command, which makes the command <name>command.
Notes:
send()understands&colour codes such as&a(green),&c(red),&7(grey) and&f(white). The global flags--no-colors,--clean-outputand--no-prefixare removed from$argsbefore your command runs.- The CLI does not boot the HTTP app or connect to the database for you. The built-in
settingscommand loads the environment with\App\App::getInstance(true)->loadEnv()and opens a connection withnew \App\Chat\Database(...)from theDATABASE_*values; do the same if your command needs the database.
Related
Frequently asked questions
The cron runner starts once a minute and calls run() on every plugin cron class each time. Use the App\Cron\Cron helper inside run() to do the real work only every N minutes, hours, days or weeks.
The CLI looks for plugin commands in backend/storage/addons below the current directory, so it only finds them when you run it from the repository root, /var/www/featherpanel, as php backend/cli. In the Docker image the working directory is the backend folder itself, where that path does not exist.