For developers
This chapter documents the programmatic surface of the translation synchronisation this extension ships: the event that triggers it, the service interface behind it, and how it behaves in workspaces. It also documents the event that lets a project decide what is written as the metadata of a profile image.
Warning
The whole surface is marked
@internal
and experimental. It works and
is covered by functional tests, but signatures may still change in a minor
release. Depend on it deliberately.
The trigger: AfterProfileUpdateEvent
\FGTCLB\ is a PSR-14 event
announcing that a profile aggregate — the profile record or one of its child
records — has changed and was persisted. This extension dispatches it after a
profile is auto-created for a frontend user
(
Abstract, also reached by the
academic: command); EXT:
dispatches it after every persisting frontend edit action, and project code —
typically a
Data hook reacting to backend edits — may dispatch it
as well to trigger the same synchronisation.
The dispatch contract:
- The event carries the persisted default language profile: its
getreturns a real uid, and the record is not a translation overlay. Listeners read the database, not the object, so all changes must be persisted before dispatching.Uid () - The profile's pid must resolve to a site — the synchronisation listener of
EXT:determines the site from the request or from the pid and skips the event silently when it cannot.academic_ persons_ edit
Updating an existing profile from its frontend user record
(
Abstract, command
academic:) dispatches the event per profile the update
runs through — announced even when every value already matched, exactly like
the frontend editing flow. A profile whose
skip_ flag is set is
neither updated nor announced.
The synchronisation surface
\FGTCLB\ declares one
method,
synchronize. The shipped
implementation (
Record) routes every write through the
TYPO3 DataHandler — nothing in TYPO3 outside the DataHandler honours
l10n_ or keeps translations consistent, so going through it
is what makes the created translations indistinguishable from ones created in
the backend: inline children, file references, MM relations,
l10n_diffsource, reference index, history and hooks are all carried along.
\FGTCLB\
describes one synchronisation run. Build it through
Synchronizer, which takes the synchronizer instance,
the
Site
, the allowed language ids, the table name and the record uid —
and silently drops language ids that are not positive or that the site does not
define, so a run never targets a language the site cannot render.
For each remaining language,
synchronize:
- creates a missing translation with a DataHandler
localizecommand — the full record, including its inline child tree; - for an existing translation, re-submits the default record's
l10n_column values as a datamap (core'smode=exclude DataMapProcessorpropagates them into every translation) and issues aninlineLocalizeSynchronizecommand per inline column, which carries child records added to the default record after the translation was created.
A missing record, a record that is not in the default language, or a record that is invisible in the acting workspace makes the run a silent no-op.
Workspace behaviour
The synchronisation acts in the workspace of the acting backend user: run
from a backend context inside a workspace, it creates versioned rows only
(t3ver_wsid set, t3ver_state=1) and never touches the live records —
publishing the workspace publishes the translations. When no backend user is
available (frontend and CLI contexts), a synthetic in-memory admin user acting
in the workspace of the current
Context
is used.
Two refusals protect the live state:
- A frontend request acting in a non-live workspace (a workspace preview) is refused entirely; a notice is logged and nothing is written. This policy is currently hardcoded.
- A uid addressing a workspace version row (
t3ver_oid > 0) is refused: the DataHandler addresses versioned records through their live uid, and accepting the version uid would publish draft values as live translations.
Image metadata: ModifyProfileImageMetadataEvent
\FGTCLB\ is
dispatched immediately before this extension writes the metadata of a profile
image, and a listener decides what is written: whatever it leaves in
get is the field map that goes to the database, and an empty
map writes nothing at all.
It is dispatched for each of the two records that carry image metadata, and
get says which one:
get |
Written when | Fields |
|---|---|---|
sys_ | a profile image is uploaded in the frontend, once, for the file that upload created — and only for the fields that record has empty |
title
,
alternative
and, with
typo3/cms-filemetadata
,
copyright
|
sys_ | the name of a profile record changes, from a backend save, a localization or a frontend edit |
title
and
alternative
|
Both records are handed over, whichever of them is written.
get is the file — its own metadata record is
$event->get — and
get is the
image relation of the profile,
null
only for a profile that has none.
get is the profile record the image belongs to, a
translation for an image of its own, and
get the request the
write happens in: the frontend request for an upload or a frontend edit, the
backend request for a save, and
null
on the command line or where the
caller has no request to pass on.
Fields the target table does not declare are dropped before the write, so a
listener may set a column unconditionally: where the installation does not have
it, the value goes nowhere.
copyright
is one such column — it belongs to
sys_ and
typo3/cms-filemetadata
, and the
relation row has no equivalent.
Warning
System fields are refused. The identity of the record, the relation it
is part of, its localization, its workspace columns and its enable columns —
uid
,
pid
,
file
,
uid_,
uid_,
tablenames
,
fieldname
,
sys_,
l10n_,
t3ver_*
,
deleted
,
hidden
and the
rest of their kind — are dropped and a warning is written to the log. This
event writes metadata; repointing a relation or moving a record is the
Data's business.
The two dispatches are not interchangeable. The metadata record of the file is written once, which makes it the place for a value that has to survive — the required attributes of typo3/cms-filemetadata or fgtclb/file-required-attributes , for instance. The reference row is rewritten on every save of the profile, so a listener that wants to own a field there has to set it on every dispatch.
<?php
declare(strict_types=1);
namespace MyVendor\MyExtension\EventListener;
use FGTCLB\AcademicPersons\Event\ModifyProfileImageMetadataEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
final class AddImageRightOfUse
{
#[AsEventListener(identifier: 'my-extension/add-image-right-of-use')]
public function __invoke(ModifyProfileImageMetadataEvent $event): void
{
if ($event->getTargetTable() !== 'sys_file_metadata') {
return;
}
$metadata = $event->getMetadata();
$metadata['right_of_use'] = 'Portrait, own use only';
$event->setMetadata($metadata);
}
}
Warning
A listener runs inside the write of the profile record — for a backend save
from within a
Data hook. Keep it short and do not write
profile records from it.
See also
- The changelog entry Translation sync is routed through the DataHandler for the behavioural differences to versions before 3.0.
EXT:, whoseacademic_ persons_ edit profile.allowedLanguagessetting feeds the allowed language ids and whose event listener wires the pieces together.