Skip to content
V
For developers
Architecture, conventions, the core and security
UI layer / Cron presenters

Cron presenters#

In this CMS a scheduled task is a presenter like any other — it is triggered by an HTTP request carrying a token and it returns JSON. The code lives in app/UI/Cron/, the base class in app/Core/Base/Cron/BasePresenter.php.

/cron/<module>/<presenter>/<action>?token=xxx

Today that is 59 actions across 9 modules (grep -rn "public function action" app/UI/Cron --include="*Presenter.php" | wc -l). Their list is generated from the code into this manual.

What the base class does for you#

Step What happens
initializeSetting() loads the settings (needed for the token)
verifyToken() compares ?token= against 1/cronToken using hash_equals(); a mismatch → 403
the concurrency lock INSERT IGNORE into cms_cron_locks under the name <presenter>.<action>
shutdown() releases the lock

Plus two response helpers: sendSuccess($data){"status":"ok", …} and sendError($message, $code){"status":"error", …}.

An empty cronToken in the settings locks EVERY task

verifyToken() rejects the request even when the expected token is empty — otherwise empty settings would mean public endpoints. On a new environment the token therefore has to be set before the crons are wired up; until then everything returns 403 and it looks like a wrong URL.

The token travels in the query string, so it gets logged

It shows up in the web server's access log and in shell history. Do not treat it as a secret on a par with a password — it is protection against an accidental run, not against an attacker with access to the logs.

Procedure: creating a new task#

  1. The presenter goes into app/UI/Cron/<Module>/Presenters/XxxPresenter.php, extends App\UI\Cron\BasePresenter, with only a manager in the constructor.
  2. The action is a few lines — call the manager and send the result:
/**
 * Recomputes the denormalised price column on all adverts.
 *
 * Run periodically, e.g. every 10–15 minutes.
 *
 * URL: /cron/bazaar/advert/recount-prices?token=xxx
 */
public function actionRecountPrices(): void
{
    $this->sendSuccess($this->advertManager->recountPrices());
}
  1. Write Frekvence: or Spouštět in the docblock — that is where the "How often" column in the manual comes from. Without it the manual build emits a warning.
  2. Write URL: in the docblock too — the generator takes the address from there.
  3. For a non-periodic task simply write "manually"; inventing a cron expression for it is worse than nothing.
  4. Add the task to the deployment checklist — otherwise the generator reports at build time that it is missing from the plan.
  5. Verify by running it — with the correct token and without one.

Logic in a cron presenter cannot be called from anywhere else, nor tested

A cron presenter injects managers only — never the service layer, generators or EntityManagerInterface. Whatever lives in the presenter exists for the cron alone; the administration will then need the same thing and it gets written twice.

Concurrency#

The lock is a row in cms_cron_locks named <presenter>.<action>. It is acquired atomically with INSERT IGNORE; beforehand, a lock past its TTL (300 s by default) is deleted, so that a crash without a release does not block the task forever. A second instance gets {"status":"skipped","reason":"already_running"}.

The lock is fail-open

When the table does not exist or the query fails, acquire() returns true and the task runs. That is deliberate — the lock must not block the cron — but it means a broken lock table looks as if the protection worked while there is none.

A long task blocks its following runs

If a reindex takes an hour and the scheduler is set to ten minutes, five runs are discarded as skipped. Choose the frequency by the actual running time, not by how often it would be nice.

A 300 s TTL is shorter than some tasks

When a task runs longer than five minutes, its lock can expire underneath it and a second instance starts in parallel. For long tasks, pass your own longer TTL.

Batches#

Tasks work in batches; the size lives in the module's settings (7/paymentBatchSize, 7/scheduleBatchSize, …), typically 10.

A smaller batch more often beats a big one rarely

An interruption in the middle of a big batch means part of the work is done twice — which is fine for idempotent operations and not fine for the rest.

Two kinds of error#

Processing an item can fail in two ways and telling them apart is mandatory:

Kind Behaviour Example
transient the settled flag is not advanced, the item is retried next time a save failure, a temporarily unavailable service
permanent the item is settled without a result and recorded in the errors for manual resolution an orphaned reference, a missing source record

Without that distinction one bad item starves the whole queue

A permanently broken item retried forever eats into the batch on every run and the others never get their turn. That is why it is settled with a note and the run moves on — see Invoicing, the section on automation.

A settled permanent error means the result is never produced

A paid order without an invoice warns nobody. The error report has to be checked, otherwise "the task completed successfully" is misleading.

Identity and permissions#

Cron bypasses the RBAC guard on the internal path

It injects the Api managers directly, not through ApiBridge — so internalMode does not apply to it. Measured: zero API_RBAC path=internal lines across six runs.

A CLI script going through ApiBridge runs as guest

That is a different thing from cron. A session-less script in enforcing mode gets no data and returns silent emptiness, not an error. It has to supply an identity itself: ApiBridge::setInternalAuthToken($jwtManager->generateToken(…)). In detail: RBAC in the API.

Fields are NOT filtered by audience in a cron

Outside a request there is no caller, so #[FieldAccess] does not apply. That is deliberate — an activation email needs activationKey — but it means whatever a cron sends outwards is its own responsibility.

Where to look#

I want Where
what the base class does app/Core/Base/Cron/BasePresenter.php
the concurrency lock app/UI/Api/System/Models/Managers/CronLockManager.php
the existing tasks app/UI/Cron/<Module>/Presenters/
the list of all tasks Scheduled tasks
the generator of that list manual/tools/gen-cron-list.py

Follow-up chapters: Scheduled tasks · Deployment checklist · RBAC in the API · Code conventions