AfterProfileUpdateEvent 

\FGTCLB\AcademicPersons\Event\AfterProfileUpdateEvent is a PSR-14 event announcing that a profile aggregate, the profile record or one of its child records, has changed and was persisted. It is the trigger of the translation synchronisation and of the slug of fgtclb/academic-persons-edit , and the place for a project to react to every change of a profile, whatever wrote it.

When it is dispatched 

  • After a profile is auto-created for a frontend user ( AbstractProfileFactory::createProfileForUser() , also reached by the academic:createprofiles command).
  • After a profile is updated from its frontend user record ( AbstractProfileFactory::updateProfileForUser() , command academic:updateprofiles ), per profile the update runs through, even when every value already matched. A profile whose skip_sync flag is set, or that a listener of BeforeProfileMappedFromFrontendUserEvent skips, is neither updated nor announced.
  • By EXT:academic_persons_edit after every persisting frontend edit action.
  • After a DataHandler save of a live, default-language profile: the backend form, and any code that writes profiles through the DataHandler. Every default-language profile in the run's datamap is announced once per run, after all of the run is written. Saves of translations alone, saves in a workspace, commands such as copy, move or localize, and a save of only a child record, such as a contract edited on its own, are not announced.

What a listener reads 

The event is read only. Besides the profile it carries the site the profile belongs to, and the origin of the update:

Method Returns
getProfile() The persisted profile in the default language.
getSite() The site the profile belongs to, when the dispatcher knows it, and null otherwise.
getOrigin() Where the update came from, a case of \FGTCLB\AcademicPersons\Event\ProfileUpdateOrigin .
getOrigin() Dispatched by getSite()
ProfileUpdateOrigin::Creation academic:createprofiles null
ProfileUpdateOrigin::Synchronization academic:updateprofiles null
ProfileUpdateOrigin::FrontendEditing the profile editing plugin of EXT:academic_persons_edit the site of the request
ProfileUpdateOrigin::Backend a DataHandler save the site of the profile's page, null when it belongs to none
ProfileUpdateOrigin::Import a DataHandler save of a run marked as an import (below) the site of the profile's page, null when it belongs to none
ProfileUpdateOrigin::Unknown code that passes no origin, such as code written for 2.x whatever it passes, usually null

The case set of ProfileUpdateOrigin is fixed, so a listener may match over it exhaustively.

The extensions register three listeners of their own, which a project listener orders itself against with the before and after arguments of #[AsEventListener] :

Identifier Extension What it does
academicPersonsUpdateProfileImageMetadata academic_persons Writes the image metadata of the profile, except for the origins Backend and Import , whose DataHandler run has written it already, see ModifyProfileImageMetadataEvent.
generateSlugForProfile academic_persons_edit Regenerates the slug of the profile.
syncChangesToTranslations academic_persons_edit Synchronises the translations, see The synchronisation surface.

Example 

A site renders the profiles in a view of its own, a teaser element for example, and tags the cached pages with my_profile_teaser_<uid>. The listener flushes them whenever a profile changes, whatever changed it:

EXT:my_extension/Classes/EventListener/FlushProfileTeasers.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\EventListener;

use FGTCLB\AcademicPersons\Event\AfterProfileUpdateEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Cache\CacheManager;

final readonly class FlushProfileTeasers
{
    public function __construct(
        private CacheManager $cacheManager,
    ) {}

    #[AsEventListener(identifier: 'my-extension/flush-profile-teasers')]
    public function __invoke(AfterProfileUpdateEvent $event): void
    {
        $profileUid = (int)$event->getProfile()->getUid();
        $this->cacheManager->flushCachesInGroupByTag('pages', 'my_profile_teaser_' . $profileUid);
    }
}
Copied!

The pages of the plugins of this extension are flushed by the extension itself.

Rules and pitfalls 

The dispatch contract, for code that dispatches the event itself:

  • The event carries the persisted default language profile: its getUid() returns 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.
  • The site is optional. Without one the synchronisation listener of EXT:academic_persons_edit determines it from the site of the global request, then from the pid, and skips the event silently when it cannot. An event of origin ProfileUpdateOrigin::Backend or ProfileUpdateOrigin::Import without a site is not synchronised at all: the DataHandler hook passes none only when the profile's page belongs to no site, and the site of a backend request is the one of the page selected in the page tree.
  • A project that dispatched the event from its own DataHandler hook to have backend saves synchronised must remove that hook: the save is announced by this extension now, and the hook would announce it a second time.

For a listener:

  • A large import is announced profile by profile, synchronously, in the request that writes it. A listener that does expensive work recognises an import by the origin ProfileUpdateOrigin::Import and defers it.
  • A listener that writes profiles through the DataHandler marks its run as ProfileWriteCorrelation::Internal , see below, or its write is announced again.

Marking a DataHandler run: ProfileWriteCorrelation 

The DataHandler hook of this extension recognises two kinds of run by an aspect of the run's correlation id, which \FGTCLB\AcademicPersons\DataHandling\ProfileWriteCorrelation creates:

ProfileWriteCorrelation::Import
An import. Its saves are announced like backend saves, once per profile and synchronously, with the origin ProfileUpdateOrigin::Import , so that a listener can tell them apart or defer its own work. Every profile is synchronised in the same request, which is the cost of a large import.
ProfileWriteCorrelation::Internal
A write of this extension itself, by the translation synchronisation or by the profile image writes. Such a run is never announced: the update it belongs to is announced by the code that started it. A listener of AfterProfileUpdateEvent that writes profiles through the DataHandler marks its run the same way: an announcement from the frontend or from a command has no DataHandler run around it, and the listener's write would be announced as a save of its own.

The synchronisation runs as the backend user of the save. An editor without access to a target language gets no translation in it, and the DataHandler error is logged, not shown.

A DataHandler run a listener starts from inside a backend save is nested in the save's run, and the DataHandler flushes the reference index of the outermost run only. Such a run passes a ReferenceIndexUpdater of its own to start() and calls update() on it afterwards, as the synchronisation does.

The mark is set after start() , which replaces the correlation id:

use FGTCLB\AcademicPersons\DataHandling\ProfileWriteCorrelation;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($datamap, []);
$dataHandler->setCorrelationId(ProfileWriteCorrelation::Import->create());
$dataHandler->process_datamap();
Copied!

A DataHandler run started from inside another DataHandler run, from one of its hooks or from a listener of the announcement it made, is never announced, marked or not.

Introduced in 

The event is part of the extension since its first release. Later changes: