therealworld / scheduler-module
The task scheduler can handle recurring tasks regularly in OXID7.
Package info
bitbucket.org/therealworld/scheduler-module
Type:oxideshop-module
pkg:composer/therealworld/scheduler-module
Requires
- php: ^8.3
- dragonmantank/cron-expression: ^3.3
- oxid-esales/oxideshop-ce: >=v7.4
- therealworld/tools-module: >=v3.4
- therealworld/tools-plugin: >=v3.9.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- v2.x-dev
- v2.5.0
- v2.4.0
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.11
- v2.2.10
- v2.2.9
- v2.2.8
- v2.2.7
- v2.2.6
- v2.2.5
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.16
- v2.0.15
- v2.0.14
- v2.0.13
- v2.0.12
- v2.0.11
- v2.0.10
- v2.0.9
- v2.0.8
- v2.0.7
- v2.0.6
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.7.24
- v1.7.23
- v1.7.22
- v1.7.21
- v1.7.20
- v1.7.19
- v1.7.18
- v1.7.17
- v1.7.16
- v1.7.15
- v1.7.14
- v1.7.13
- v1.7.12
- v1.7.11
- v1.7.10
- v1.7.9
- v1.7.8
- v1.7.7
- v1.7.6
- v1.7.5
- v1.7.4
- v1.7.3
- v1.7.2
- v1.7.1
- v1.7.0
- v1.6.8
- v1.6.7
- v1.6.6
- v1.6.5
- v1.6.4
- v1.6.3
- v1.6.2
- v1.6.1
- v1.6.0
- v1.5.6
- v1.5.5
- v1.5.4
- v1.5.3
- v1.5.2
- v1.5.1
- v1.5.0
- v1.4.14
- v1.4.13
- v1.4.12
- v1.4.11
- v1.4.10
- v1.4.9
- v1.4.8
- v1.4.7
- v1.4.6
- v1.4.5
- v1.4.4
- v1.4.3
- v1.4.2
- v1.4.1
- v1.4.0
- dev-master
This package is auto-updated.
Last update: 2026-10-26 15:47:40 UTC
README
Deutsche Fassung: README.de.md

description
A task scheduler for the shop. Other modules ship tasks (exports, imports, clean-ups, ...);
the scheduler finds them, keeps them in a list in the backend and runs them either on a crontab
schedule — triggered by a server cron job calling OXID's bin/cron.php — or manually from the
backend. Long tasks run in steps, so they fit into the PHP runtime limit and carry on where they
stopped.
The module also takes over OXID's "upcoming prices" job (switching oxupdateprice to oxprice),
which the core otherwise runs on every frontend request — see Changes to the shop
core.
- Composer package:
therealworld/scheduler-module - OXID module id:
trwscheduler
Requirements and installation
- OXID eShop CE
>=v7.4, PHP^8.3 therealworld/tools-plugin>=v3.9.0therealworld/tools-module>=v3.4(moduletrwtoolsmust be active, seedependencies.yaml)dragonmantank/cron-expression^3.3
The task table and the log table are created by migrations, so the module needs install, migrate and activate. With the clirun-plugin this is one command:
composer require therealworld/scheduler-module
vendor/bin/oe-console trw:module:setup trwscheduler
Without the clirun-plugin:
vendor/bin/oe-eshop-db_migrate migrations:migrate trwscheduler
vendor/bin/oe-console oe:module:activate trwscheduler
Upgrading to v2.4
v2.4 stores tasks under their fully qualified class name (see Task identity) and brings two new settings. After the update, refresh the module and convert the existing tasks once with this SQL statement (no migration; running it twice changes nothing):
vendor/bin/oe-console trw:module:refresh trwscheduler
UPDATE `trwschedulertasks`
SET `OXTASK` = `OXNAMESPACECLASSNAME`
WHERE `OXTASK` <> ''
AND INSTR(`OXTASK`, CHAR(92)) = 0
AND RIGHT(`OXNAMESPACECLASSNAME`, CHAR_LENGTH(`OXTASK`) + 1) = CONCAT(CHAR(92), `OXTASK`);
Without the update the module keeps working: unconverted rows are found via
OXNAMESPACECLASSNAME and converted whenever look for new tasks saves them again.
Upgrading to v2.5
v2.5 adds the download of export files from the log (see Backend). No migration, just refresh the module:
vendor/bin/oe-console trw:module:refresh trwscheduler
ISchedulerTask gained the method getPathForDownloadFiles() for this. Tasks extending
AbstractSchedulerTask inherit its default (no downloads); a class implementing the interface
directly has to add it.
Getting started
- In the module settings, group Cronjob, switch on cron job processing
(
bTRWSchedulerCronjobActive). Without it no task is ever run by the cron job. Set up a server cron job that calls OXID's cron script, ideally every minute:
* * * * * php /path/to/shop/source/bin/cron.phpEach call runs one step of one task at most. The crontab of a task only says from when on it is due; how soon it actually runs depends on how often this cron job fires.
- Backend Service → Task-Scheduler: click look for new tasks. The scheduler scans the active modules and adds every task it does not know yet — inactive.
- Activate the tasks you want, check their crontab, and decide per task between Cronjob yes (scheduled) and Cronjob no (manual only).
Activate at least Update Upcoming Prices if the shop uses prepared prices
(oxupdateprice): with this module active, nothing else switches them.
How a run works
On every call of bin/cron.php the scheduler:
- releases a stale lock (see
iTRWSchedulerLockTimeout) and logs that it did; - answers busy if another run still holds the lock;
- picks one task: first an active scheduled task that is still in progress from an earlier call, otherwise the active scheduled task whose next start is the longest overdue;
- runs one step of it within the runtime budget;
- if the task reports further steps, it stays in progress (shown as paused) and continues on the next call; otherwise its next start is calculated from its crontab.
If a task throws, the error is logged (shop log and scheduler log), the run is discarded and the task starts over at step 0 on its next regular date — one broken task no longer blocks all others. The lock is released in any case.
Runtime budget per step: the setting for the console or the browser, capped at PHP's
max_execution_time (if one is set), minus 5 seconds reserved for writing the log and the
response, but at least 5 seconds.
Backend: Service → Task-Scheduler
- Task list — per task: title, active/inactive, Cron/manual, the crontab, the next run, and whether it is running or paused. Active, scheduled-or-manual and crontab are saved in place. A task without a default crontab is always manual.
- start manually — for active manual tasks. In the browser a long task runs step by step:
with
bTRWSchedulerManualAutoReloadthe page reloads itself for the next step, otherwise use manual reload. If a step does not come back in time, the page says so; the processed part is saved and the task resumes where it stopped when it is started again. Manual starts are blocked while a task is running; the settings stay editable. - Reset — for a running task: releases the lock and takes the task out of progress. Data a task has already changed is not undone.
- upload — for manual tasks that work on files (the task defines the target directory); the
files already waiting are listed. Only files of the types
csv,tsv,txt,xml,json,zip,gzare accepted (SchedulerController::UPLOAD_EXTENSIONS); anything else, and any name containingphp,phtml,phar& co., is rejected with a message in the log. - download — links in log messages of tasks that write files (e.g. exports). The files usually lie outside the document root and have no public URL; the link delivers them to logged-in admins only, and only files directly in the download directory the task names. The scheduler adds the session token only when it shows the log, so links from cron runs in the database log work too. If the file is gone, the log says so.
- Next potential Cronjob-Task — which task the cron job would pick next.
- show Database-Log (paged, newest first) and clean Database-Log.
- look for new tasks and remove all inactive tasks (deletes the list entries; a task that is still present is found again by the next search).
Settings
| Group | Setting | Default | Effect |
|---|---|---|---|
| Cronjob | bTRWSchedulerCronjobActive — enable cron job processing | off | master switch for scheduled runs. Off: the cron job finds nothing to do; manual runs still work |
| Runtimes | iTRWSchedulerRunTimeBrowser — maximum execution time for browsers (s) | 30 | budget per step of a manual run in the backend |
| Runtimes | iTRWSchedulerRunTimeConsole — maximum execution time for console scripts (s) | 150 | budget per step of a cron run |
| Runtimes | bTRWSchedulerCheckStillRunning — check if the task scheduler is already running | on | runs one task at a time (lock). Off: runs may overlap |
| Runtimes | iTRWSchedulerLockTimeout — release a stuck lock automatically after (minutes) | 60 | a run killed from outside (memory or time limit, host) leaves its lock behind; after this age the lock counts as stale. Never shorter than one step may take. 0 = never |
| Manual Run | bTRWSchedulerManualAutoReload — start a necessary reload automatically | on | see start manually |
| File options | iTRWSchedulerFileAgeForImport — minimum age of imported files (minutes) | 360 | an import task only touches files older than this, so it does not read a file still being uploaded. Applies to import tasks that ask for it: a module's import task that reads files from an upload directory can call SchedulerConfig::getFileAgeForImport() and skip younger files (see Helpers). 0 = no check |
| Log output | bTRWSchedulerLogWriteToDb — write the log into the database | on | keeps the log of each run for the backend; off: the log ends with the request |
| Log output | sTRWSchedulerLogLevel — log threshold | info | error, warning, notice, info, debug. One threshold for what is written and what the log view shows. Use debug for troubleshooting only — the lines are written at the end of each run and cost runtime |
| Log output | iTRWSchedulerLogLoadMaxEntries — maximum log entries per page | 200 | page size of the log view; 0 or more than 5000 is capped at 5000 |
| Log output | iTRWSchedulerLogKeepDays — keep log entries for (days) | 90 | the scheduler deletes older log lines of the shop by itself, at most once a day as part of a run. 0 = never delete |
Logging
Each run collects its messages (task started, paused at step x/y, continued, stopped, aborted,
stale lock released, new task installed) and writes them in one go at the end of the run into
the table trwschedulermonolog, filtered by the log threshold. Entries older than
iTRWSchedulerLogKeepDays days are deleted by the scheduler itself: at the end of a run (cron or
manual), at most once a day, for the current shop only. clean Database-Log still empties the
whole table. Errors from a crashing task additionally go to the shop log.
Built-in tasks
| Task | Title | Default crontab | What it does |
|---|---|---|---|
TaskUpdateUpcomingPrices | Update Upcoming Prices | 10 2 * * * | switches prepared prices (oxupdateprice) to the current price, forced |
TaskDummy | Hello World! | 10 2 * * * | writes one log line; for testing the setup |
Changes to the shop core
ShopControl::executeMaintenanceTasks()is switched off. In the core it runs on every request and only updates upcoming prices; with this module that is the job of the task Update Upcoming Prices alone.Maintenance::execute()(called bybin/cron.php) runs the scheduler instead of the core's price update.
Task identity and shops
- A task is identified by its fully qualified class name (
oxtask, e.g.TheRealWorld\SchedulerModule\SchedulerTasks\TaskDummy). Two modules may therefore ship task classes with the same short name. Title key and log channel stay with the short name. - Code that addresses a task from outside (
SchedulerHelper::deleteTask(),SchedulerHelper::getTaskOxId()) passes the fully qualified name. A short name is still accepted as long as exactly one task of the shop carries it; if it is ambiguous, nothing happens. - Existing entries are converted by the one-off v2.4 SQL update. Entries whose class cannot be resolved stay unchanged; they are still found and are lifted by the next look for new tasks once their class is back.
- Task list, cron selection, running task and remove all inactive tasks only cover the
current shop (
oxshopid); newly found tasks are created for it. The shop comes from the OXID context — on the console forbin/cron.phpas well; in CE it is always shop 1. Entries from before v2.4 carry shop 1.
Limits
- One step of one task per cron call. Several due tasks are worked off one after the other, so the cron interval sets the throughput.
- New tasks only appear after look for new tasks; they are never added on module activation.
- The crontab of a task can only be as precise as the server cron job that triggers the scheduler.
For developers
Writing a task
A module provides a task by placing a class in <module>/src/SchedulerTasks/:
- file name
Task*.php, class name = file name, one class per file, PSR-4 autoloadable; - not abstract, implements
TheRealWorld\SchedulerModule\Core\ISchedulerTask— usually by extendingAbstractSchedulerTask; - title: a translation key equal to the upper-cased short class name (e.g.
TASKMYIMPORT) in the module'stranslations/<lang>/language file. Use the frontend translations: on the console the admin language files are not loaded; $sDefaultCrontab: the default schedule (* * * * *format). Empty = manual-only task;install(): called once when the task is first found.
namespace MyVendor\MyModule\SchedulerTasks;
use TheRealWorld\SchedulerModule\Core\AbstractSchedulerTask;
class TaskMyImport extends AbstractSchedulerTask
{
protected string $sDefaultCrontab = '*/15 * * * *';
public function run(int $iCurrentStep = 0, int $iCurrentSupStep = 0, bool $bRunManually = false): bool
{
$iOffset = $iCurrentSupStep;
foreach ($this->loadRecordsFrom($iOffset) as $aRecord) { // your own code
$this->importRecord($aRecord); // your own code
$iOffset++;
if ($this->isRunTimeOver()) {
// pause: the scheduler calls run() again with this sub step
$this->iNextSubStep = $iOffset;
return true;
}
}
// next step 0 and sub step 0 = the run is finished
$this->iNextStep = 0;
$this->iNextSubStep = 0;
return true;
}
}
Steps: after run() the scheduler reads getNextStep() and getNextStep(true). If either is
greater than 0, the task stays in progress and run() is called again with these values on the
next cron call or reload; 0/0 ends the run. The return value of run() is not evaluated.
isRunTimeOver() checks the deadline the scheduler stored in the session and never fires when
there is none.
Log via ToolsLog::setLogEntry($sMessage, $sChannel, $sLevel) from the tools-plugin; the
entries end up in the scheduler log.
Manual file tasks: override getPathForManualFiles() (target directory of the upload) and
getManualFileList() (files waiting). The upload form appears for active manual tasks without a
default crontab. The overview asks every task through SchedulerTask::getManualFilesPath() /
getManualFiles(), which catch an Error thrown by a task (e.g. a typed $aPathConfig read
before initialisation), log it and show no upload field for that task instead of breaking the
whole page. Still give such properties a default (protected array $aPathConfig = [];).
Tasks with files to download: override getPathForDownloadFiles() (directory relative to the
shop root like the manual files, e.g. ../var/<module>/export/) and put
$this->getDownloadLink($sFileName) as the href into the log message. The link is relative and
already HTML-escaped; SchedulerController::render() adds the admin self link with the token when
it shows the log, SchedulerController::downloadTaskFile() delivers the file. The task is looked
up by its fully qualified class name, the file only by its name without a path.
public function getPathForDownloadFiles(): string
{
return MyConfig::getPathConfig()['exportPath'];
}
// ...
ToolsLog::setLogEntry(
sprintf('Export %s created: <a href="%s">download</a>', $sFileName, $this->getDownloadLink($sFileName)),
__CLASS__ . ' - ' . __FUNCTION__,
ToolsLog::LOG_LEVEL_INFO
);
Helpers
AbstractArticleExportTask: fixed five-step run for "collect → CSV → zip → send" exports (resetData()at step 0, thengenerateData(),exportData(),zipData(),sendData()for steps 1–4;initExport()before each step).SchedulerConfig::getFileAgeForImport(): the minimum file age setting for import tasks.
Internals
- Tables (migrations in
migration/data/, tableoxmigrations_trwscheduler):trwschedulertasks(task list, state, current step, crontab, next start) andtrwschedulermonolog(run log, taken over from the tools-module). Since v2.4trwschedulertasks.OXTASKholds the fully qualified class name. - Class extensions:
OxidEsales\Eshop\Core\ShopControl,OxidEsales\Eshop\Application\Model\Maintenance. - Admin controller
SchedulerController(controller keyschedulercontroller, lowercase as inmenu.xml, and so is everycl=in the template and the reload URL), template@trwscheduler/admin/scheduler, block extension of the adminheaditem.html.twigfor the JS. - Assets: sources in
resources/(Vite, SCSS, vanilla JS), build output inassets/src/.