Skip to content

Events & Flow Builder

The extension dispatches exactly one custom event: a terminal job notification that you can react to from the Flow Builder or from PHP. It also registers two scheduled tasks and two subscribers.

Flow Builder trigger

Nice\Translate\Job\Event\TranslationJobFinishedEvent is a registered business event, therefore it appears in the trigger list of the Flow Builder.

AspectValue
Technical namenice_translate.job.finished
Label in the AdministrationTranslation job finished
Trigger pathAI TranslationJobFinished
ClassNice\Translate\Job\Event\TranslationJobFinishedEvent
Base classSymfony\Contracts\EventDispatcher\Event

It implements three aware interfaces:

InterfaceConsequence
Shopware\Core\Framework\Event\FlowEventAwareThe event can be a flow trigger at all
Shopware\Core\Content\Flow\Dispatching\Aware\ScalarValuesAwareThe extension persists the six payload values with the flow sequence, therefore a storable, deferred flow execution still has them
Shopware\Core\Framework\Event\MailAwareThe mail action of Shopware can address the creator of the job, and you configure no recipient

getSalesChannelId() always returns null. Translation jobs are administration-level work without a sales channel scope.

The Flow Builder editor with the trigger set to the AI Translation job-finished event

Payload

getAvailableData() exposes exactly these six scalars, in this order. getValues() returns the same six keys, which makes them available to a deferred flow execution and to a mail template.

PropertyTypeMeaning
jobIdstringThe hex UUID of the nice_translate_job row
titlestringThe job title, as the job list shows it
statusstringThe terminal status that the job reached
processedItemsintThe records that the extension translated
failedItemsintThe records that failed
skippedItemsintThe records that the safety gates skipped

The extension finalises a job after processedItems + failedItems + skippedItems reaches the planned total. The three counters together therefore account for the full run. A record counts as skipped when a safety gate refused to overwrite it. See Safety mechanisms.

When it fires

The event carries one of three statuses:

StatusReached when
completedThe extension processed each record, and none failed
completed_with_errorsThe job finished with a minimum of one failed record
failedThe preparation of a queued job failed before any batch ran

The batch finaliser produces the first two statuses only. failed arrives only through JobProgressUpdater::failQueued(), which writes its own snapshot to the same outbox.

A cancelled job emits no event. On-save automation also emits none, because it translates without a job. See Automation.

The finalizer writes the terminal snapshot into the nice_translate_finished_event outbox, in the same transaction that finalises the job, and it never dispatches the event itself. JobFinishedEventRelay collects the snapshot after that commit. The delivery is therefore at-least-once: a dispatch that throws keeps its lease, and the scheduled recovery task retries it after the stale window. Deduplicate on jobId if your listener has side effects.

Mail recipients

The relay derives the recipient from the creator of the job, that is, the Shopware user that nice_translate_job.created_by_id refers to. That address travels with the event, which lets a Flow Send mail action reach the creator without further configuration.

WARNING

The scheduled task creates its jobs under a system context, and those jobs have no creator. The recipient list is therefore empty for them. Configure an explicit recipient in your mail action if you want a notification about the scheduled runs.

Build a flow

These steps build a flow that reports the jobs that finished with errors:

  1. In the Administration, open the Flow Builder and create a new flow. Give it a name such as Translation job finished.
  2. As the trigger, select AI TranslationJobFinished. The list shows it as Translation job finished.
  3. Add a mail action. The event is MailAware, therefore the action can address the creator of the job. It also accepts all administrators, or a fixed address.
  4. In the mail template, branch on the payload, so that a clean run sends no mail:
twig
{% if failedItems > 0 %}
    Translation job "{{ title }}" finished as {{ status }}.
    {{ processedItems }} processed, {{ failedItems }} failed, {{ skippedItems }} skipped.
    Open the job detail view for the error list: job id {{ jobId }}.
{% endif %}

The trigger fires for each job that reaches a terminal status, and it has no filter of its own. The condition therefore belongs in the template, or in a PHP listener.

Scheduled tasks

The extension registers two scheduled tasks with the shopware.scheduled.task tag. Both need an active bin/console scheduled-task:run worker.

Task classTask nameDefault interval
AutoTranslateTasknice_translate.auto_translate3600 s (1 hour)
BatchDispatchTasknice_translate.dispatch_batches60 s (1 minute)

nice_translate.auto_translate

The task ticks hourly and translates a maximum of once per scheduledIntervalHours. AutoTranslateTaskHandler::run() performs these steps on each tick, in order:

  1. Prune the history. It deletes the nice_translate_history rows that are older than historyRetentionDays. This happens on each tick, with scheduled translation on or off. For a configured value below 1, the retention falls back to 30 days.
  2. If scheduledEnabled is off, return. Nothing below this point operates.
  3. If the configured interval did not pass since scheduledLastRun, return. For a configured value below 1, scheduledIntervalHours falls back to 24.
  4. If scheduledEntities or autoTranslateLanguageIds is empty, remember the run and return.
  5. If any nice_translate_job is still queued or running, return without a record of the run. The run stays due, and the next tick makes the same check again.
  6. Group the target languages by their resolved provider, and create one job for each provider group. One job per provider keeps the validation, the accounting and the job labels unambiguous. The jobs use the scope missing, the mode missing, skipUnchanged and protectManualEdits, and their title starts with Scheduled (<provider label>): .
  7. Write scheduledLastRun as an ISO-8601 timestamp, after the creation of a minimum of one job. A due run that created no job keeps the timestamp unchanged, therefore the next tick tries again.

The entity list of this task is scheduledEntities, a setting separate from the autoTranslateEntities that on-save automation reads. Automation and the Settings reference document both settings.

nice_translate.dispatch_batches

BatchDispatchTaskHandler::run() calls JobBatchDispatcher::recover(), then JobFinishedEventRelay::recover(). Each call has its own try/catch that logs the failure, therefore one failing recovery pass does not block the other pass.

This task makes the durability layer operate. It enqueues the committed jobs whose start message never reached the transport, relays the batches with an incomplete dispatch again, retries the stale finished-event claims and prunes the expired receipts. It is infrastructural and has no settings.

Subscribers

src/Job/Subscriber/ contains two subscribers, and both are registered as kernel.event_subscriber.

ClassSubscribes to
BusinessEventCollectorSubscriberBusinessEventCollectorEvent::NAME, priority 1000
EntityWrittenSubscriberproduct.written, category.written, product_manufacturer.written, cms_page.written

BusinessEventCollectorSubscriber defines TranslationJobFinishedEvent and adds it to the collection, which makes the trigger appear in the Flow Builder.

EntityWrittenSubscriber implements the on-save automation. It walks a guard chain before it dispatches anything, in this order:

  1. autoTranslateEnabled must be true.
  2. The context must not carry the state nice_translate.writing. This is the loop guard, and it stops the writes of the extension from a second trigger of the subscriber.
  3. The write must have happened in the system default language.
  4. The write must have happened in the live version.
  5. autoTranslateEntities must contain the entity.
  6. autoTranslateLanguageIds must not be empty.
  7. The subscriber drops the delete results and deduplicates the remaining ids.

It then resolves a provider for each target language, builds a jobless JobConfig with the scope selection, the mode missing, skipUnchanged and protectManualEdits, divides the ids into batches of the configured size, and registers a durable manifest before the dispatch. If it cannot resolve a provider, it logs the failure and skips that language. It also logs a dispatch failure and then leaves the work to the recovery task.

TranslationJobFinishedEvent is the only custom event class in the extension, and the extension dispatches nothing else. If you need a hook that does not exist, the provider tag from Custom providers is usually the correct place.

Listen in PHP

The relay dispatches with an explicit event name:

php
$this->eventDispatcher->dispatch($event, TranslationJobFinishedEvent::EVENT_NAME);

Key your subscriber on that name. A subscriber that you register against the class does not receive the dispatch:

php
<?php declare(strict_types=1);

namespace Acme\Translate\Subscriber;

use Nice\Translate\Job\Event\TranslationJobFinishedEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class TranslationJobSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            TranslationJobFinishedEvent::EVENT_NAME => 'onJobFinished',
        ];
    }

    public function onJobFinished(TranslationJobFinishedEvent $event): void
    {
        if ($event->getFailedItems() === 0) {
            return;
        }

        $this->logger->warning('Translation job finished with errors.', [
            'jobId' => $event->getJobId(),
            'title' => $event->getTitle(),
            'status' => $event->getStatus(),
            'processed' => $event->getProcessedItems(),
            'failed' => $event->getFailedItems(),
            'skipped' => $event->getSkippedItems(),
        ]);
    }
}
xml
<service id="Acme\Translate\Subscriber\TranslationJobSubscriber">
    <argument type="service" id="logger"/>
    <tag name="kernel.event_subscriber"/>
</service>

Two properties of the dispatch are important for each listener that you write:

  • It operates in a queue worker. The relay dispatches from the process that finalised the job: a queue worker, or the scheduled recovery task. There is no web request around it, and getContext() returns a context from a system source. There is therefore no administration user and no sales channel to read from it.
  • It can be called more than one time for the same job. The delivery is at-least-once. Keep the listener idempotent, or deduplicate on jobId.

If you need more than the six scalars, load the nice_translate_job entity by jobId through its repository. See Admin API for the entity list and its necessary privileges.

modernice extensions for Shopware 6. Shopware is a trademark of shopware AG — this documentation is not affiliated with shopware AG.