Academic Persons 

Extension key

academic_persons

Package name

fgtclb/academic-persons

Version

main

Language

en

Author

FGTCLB

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Wed, 09 Sep 2026 00:44:00 +0000


This TYPO3 extension adds a personal database to TYPO3, with requirements that are usually used for colleges, universities or public institutions.

The profile data records can be automatically created and linked in conjunction with the LDAP extension on the basis of FE user data records. The data can be enriched with data via separate HiS-in-One synchronization. Data records can be created, edited and displayed in the front end and in different display modes.


Introduction 

What the extension does and the main concepts behind it.

Installation 

Install academic_persons via Composer, the Extension Manager or a TER upload.

Upgrade 

The ordered steps from 2.4 to 3.0.0: schema update, upgrade wizards, settings migration, templates, and the columns to drop last.

Configuration 

Configure the extension and its plugins for your installation.

Templates 

Override and customise the frontend templates.

For developers 

The translation synchronisation surface: the trigger event, the service interface and the workspace behaviour.

Known problems 

Known issues and information about them.

Changelog 

Learn about what has changed and which actions are required to upgrade.

What does it do? 

This extension provides record types for academic person profiles using the TYPO3 backend (list module/page layout module) to manage the profiles requiring backend user account(s).

Further, plugins are provided to display the persons in the frontend as a list view and detail view for each person.

The following profile data is available to users after installation:

  • Master data:
    • Salutation / Gender
    • title
    • First name
    • Last name
    • Middle name
    • Website + website link
    • Image
    • URL
  • Contracts:
    • Each person can receive any amount of contract data in order to be displayed in individual roles, functions or organizational units.
    • Position
    • Organizational unit / department - link to own data type
    • Contract start / end
    • Location - e.g. for campus Link to own data type
    • Room information
    • Office hours
  • Address data
  • Email addresses
  • Telephone addresses
  • Linked pages (in combination with the Contact-For-Pages extension)
  • Employment category based on system categories
  • Profile text data:
    • All textual content can be freely designed using the standard text editor.
    • Learning areas/fields of activity
    • Research areas
    • Supervised dissertations
    • Supervised doctoral theses
    • Miscellaneous information
  • Profile timeline entries
    • All timeline entries allow the chronological presentation of content, usually with a year or a start and/or end year, a title, a short description and a link
    • Research projects
    • Academic career
    • Memberships/committee activities
    • Networks and cooperation's
    • Publications
    • Lectures
    • Press/Media Publications

Installation 

The extension has to be installed like any other TYPO3 CMS extension. You can download and install it using one of the following methods.

Version 3 requires TYPO3 13.4 or TYPO3 14.3, and PHP 8.2 or newer. Updating an existing 2.4 installation is an ordered sequence rather than a single command - see Upgrading from 2.4 to 3.0.0.

Install the stable release
composer require 'fgtclb/academic-persons':'^3'
Copied!
  1. Switch to the module Admin Tools > Extensions.
  2. Switch to Get Extensions.
  3. Search for the extension key academic_persons.
  4. Import the extension from the repository.
  1. Get the current version from TER by downloading the ZIP version. Alternatively, get the ZIP from the GitHub Releases page.
  2. Switch to the module Admin Tools > Extensions.
  3. Enable Upload Extension.
  4. Select or drag the extension ZIP archive and upload the file.

Upgrading from 2.4 to 3.0.0 

Version 3.0.0 changes the shape of Configuration/AcademicPersons/Settings.yaml, the public detail template and - together with fgtclb/academic-persons-edit - the complete profile editing frontend. Each of those changes carries its own changelog entry with the detail; this page is the order they have to be applied in.

The order matters. One wizard reads a database column TYPO3 v14 no longer has, and one repairs relations the new editor then writes. Work through the steps from top to bottom, on a copy of the production database first.

The steps at a glance 

Step What it does What happens if it is skipped
  1. Update the packages
Installs 3.0.0 of every academic extension of the installation. Nothing below applies.
  1. Run the plugin migration wizards, still on TYPO3 v13
Moves the list_type plugin records onto their own CType. The content elements stop rendering on TYPO3 v14, where the column the wizards read no longer exists.
  1. Update the database schema
Applies the column changes of 3.0.0, among them the unsigned timeline year columns and the workspace columns. The wizard of step 4 finds no repaired schema and the editor writes into columns the installation does not have.
  1. Repair the profile image relations
Reduces duplicate references, corrects relation counters and marks the translations that carry an image of their own. A translation loses its own image at the next synchronisation, and duplicate references keep rendering the wrong file.
  1. Migrate the settings override
Replaces the pre-3.0 keys of a site package with the section maps. The installation runs on the legacy overlay, which is removed in 4.0 - and a renamed type or fieldName stays silently broken.
  1. Adapt templates, icons and TypoScript
Re-applies project overrides to the new template tree and makes the JSON page type reachable. The editor cannot save, and an overridden detail view loses the configurable layout.

1. Update the packages 

The academic extensions are released together and depend on each other, so they are updated in one go:

composer require 'fgtclb/academic-persons':'^3' 'fgtclb/academic-persons-edit':'^3'
Copied!

In a classic (non-Composer) installation, update every academic extension in the Extension Manager and activate rte_ckeditor, which academic_persons_edit requires for its rich text fields since 3.0.0.

Flush all caches afterwards. The settings graph is cached in the core cache and the cache identifier changed with the file format, so a stale entry is not read back - but the TypoScript and TCA caches are.

2. Run the plugin migration wizards while still on TYPO3 v13 

academicPersons_MigrateListTypeToCTypeContentElements and academicPersonsEdit_pluginContent move the content elements of both extensions from CType = list plus list_type onto their own CType. Both read tt_content.list_type , and TYPO3 v14 removed that column.

vendor/bin/typo3 upgrade:run academicPersons_MigrateListTypeToCTypeContentElements
vendor/bin/typo3 upgrade:run academicPersonsEdit_pluginContent
Copied!

Run them on TYPO3 v13, before the core update. TYPO3 v14 removed tt_content.list_type from the TCA, so the database analyzer of a v14 installation offers the column for removal and a fresh v14 installation never has it. Both wizards ask the live schema: once the column is gone their content element half finds nothing to do, and the records they would have migrated keep a content type nothing renders. The academicPersons_ wizard also migrates the explicit_allowdeny values of the backend user groups, and that half stays available on both versions.

academicPersonsEdit_removeProfileSwitcherContent deletes the content elements of the removed profile switcher plugin and handles both shapes, so it can be run on either core version.

3. Update the database schema 

vendor/bin/typo3 extension:setup
Copied!

In the backend the same thing is Admin Tools > Maintenance > Analyze Database Structure. It applies every column change of 3.0.0. Nothing is converted here and no value is rewritten: the timeline keeps its year , year_start and year_end columns, which only turn unsigned because the corrected TCA declares a lower bound of 0.

4. Repair the profile image relations 

Only relevant where academic_persons_edit is installed, and only worth running where profiles have images.

vendor/bin/typo3 upgrade:run academicPersonsEdit_repairLocalizedProfileImages
Copied!

The profile image column is translatable from 3.0.0 on. Until then it was excluded from localisation and the upload path wrote the relation rows by hand, which left three shapes behind that are defects under the new model: duplicate references on one profile, a relation counter that disagrees with the number of references, and a translation carrying its own reference without the custom localisation state - the state that keeps the next synchronisation from replacing it with a localisation of the default-language image.

The wizard repairs all three through the TYPO3 DataHandler, so the reference index, the record history and the localisation state are the core's. No file is deleted; a file left without a relation is for the unused files tooling of the Install Tool to report. Prefer the command line over the Install Tool module for it.

Run it after step 3 and before editors start working in the new editor of a multilingual installation. It is repeatable as well; where a 3.0.0 pre-release recorded it as done, vendor/bin/typo3 upgrade:mark:undone academicPersonsEdit_repairLocalizedProfileImages offers it again. See Breaking: The profile image translates.

5. Migrate the settings override 

Only relevant for an installation whose site package ships Configuration/AcademicPersons/Settings.yaml.

The pre-3.0 top-level keys validations and profileInformationsTypes are mapped onto the four section maps at runtime, with a warning in the log, so such a file keeps working. The overlay is transitional and is removed in academic_persons 4.0.

vendor/bin/typo3 academic:persons:settings:migrate
Copied!

The command prints, for every active package that still ships a legacy key, the profile , special , contracts and documentSections maps the overlay produces - the document that replaces the legacy keys of that package. It never writes the file, and it exits with 1 while such a package exists, so a deployment pipeline can gate on it. Paste the printed maps into the site package, review them, and flush all caches.

Migrating a pre-3.0 override has the complete key mapping and what is deliberately not mapped; Breaking: Section-based AcademicPersons settings describes the new shape.

6. Adapt templates, icons and TypoScript 

The public detail view 

Resources/Private/Templates/Profile/Detail.html was rewritten: it is a dispatcher over the profile.structure and profile.details layout, and every element is one partial below Resources/Private/Partials/Profile/PublicProfile/.

An installation that overrides the detail template keeps rendering its own copy, so nothing looks broken - and it loses the configurable layout completely. The timeline properties it reads are unchanged: {item.year}, {item.yearStart} and {item.yearEnd} are still there and still integers. Two of the partials the old detail template rendered are now only used by the list and card views, and the third, Partials/Profile/DataHeader.html, is deleted - a project template that still renders it fails at render time. See What an override of the detail template loses and Breaking: The partials of the detail view change.

The profile editing view 

The editing plugin of academic_persons_edit was replaced in place. Every Fluid file of the removed form flow is gone, so an override of one of them renders nothing; the new tree is Templates/Profile/Index.html with the partials below Partials/Profile/, and the markup of the two regions the browser builds is authored in Fluid as <template data-pe-proto> prototypes. The profile editing chapter of that extension names every file, every hook and the four prototype attributes an override has to keep.

Icons 

The icon set of the editor was replaced. Five identifiers of the form flow are gone - academic-persons-edit-add-image, -add-item, -cancel, -sort and -to-top - and the thirteen action icons of the new set are registered in Configuration/Icons.php of academic_persons_edit, under the identifiers listed in the icon table. A template or PHP file addressing a removed identifier renders TYPO3's default-not-found placeholder. The new icons are inlined as <svg> rather than emitted as <img>, so they follow the text colour - and a site stylesheet that selects .t3js-icon img no longer matches them.

TypoScript and the JSON page type 

The write path of the editor is a PAGE object with typeNum = 1733735 , which is delivered by the site set fgtclb/academic-persons-edit-profile-editing or by the static template Academic Persons Edit: Profile editing. There was no such page type in 2.4, because the old editor was a server-rendered form flow.

Check that the site really includes one of the two. A site package that copied the extension's TypoScript into its own instead of including it renders the new editor and would answer every save with the page's HTML instead of JSON. The editor detects that case: where the request carries no such PAGE object it renders a role="alert" message above itself and logs the cause, naming the site set. Such a site package has to add the academicPersonsProfileEditingAjax object by hand, or include the delivered TypoScript.

A PageType route enhancer has to map the page type, and a web application firewall or reverse proxy has to let it and the X-Requested-With header through - see the page type section.

Verifying the result 

  1. A timeline entry of a profile shows its year in the frontend and in the backend record editor, and the backend form rejects a year above 9999.
  2. The profile editing plugin loads without the "cannot be saved" alert above it, a field can be saved, and the browser console shows no failed request to the page type 1733735.
  3. A profile image is shown in every language of a translated profile, and uploading a new one in one language does not change the other.
  4. vendor/bin/typo3 academic:persons:settings:migrate exits with 0, and the log carries no legacy settings warning.

Configuration 

This extension ships its frontend TypoScript and its backend page TSconfig in two forms: as TYPO3 site sets, and as classic static templates plus page TSconfig files that are selected on a page. Both forms read the very same files, so they configure an installation identically.

Pick one of them per site and stay with it — see Do not combine both for what happens otherwise.

What the sets contain 

This extension ships six content elements, so it ships six component sets and one aggregate set that depends on all of them.

All six are driven by one Extbase plugin, so they share one TypoScript block, plugin.tx_academicpersons . That block is shipped once, in Configuration/TypoScript/Default/, and every component includes it. Which component sets a site names therefore decides which content elements the backend offers, not how much TypoScript is loaded.

Set Delivers
fgtclb/academic-persons-list The Persons List content element.
fgtclb/academic-persons-list-and-detail The Persons List and Detail content element.
fgtclb/academic-persons-detail The Persons Detail content element.
fgtclb/academic-persons-card The Contacts content element, and the FlexForm restriction that hides the list, sorting and pagination fields for it.
fgtclb/academic-persons-selected-profiles The Profiles: Selected Profiles content element.
fgtclb/academic-persons-selected-contracts The Profiles: Selected Contracts content element.
fgtclb/academic-persons Everything above. This is the set to use unless you deliberately want a subset.
fgtclb/academic-persons-default The name this extension published before the sets were cut per component. It delivers exactly what fgtclb/academic-persons delivers, and is kept so that existing site configurations keep working.
fgtclb/academic-persons-standalone Everything the aggregate delivers, plus a page object that renders content on a plain Bootstrap page. Meant for an installation without a site package of its own — an alternative to fgtclb/academic-persons, never an addition to it.

Every component set depends on fgtclb/academic-base-ctype-group, the set of EXT:academic_base that labels the content element group all academic extensions sort their elements into.

The site settings of this extension — the detail page, and the default grouping, sorting and pagination of a profile list — are declared with the aggregate set. A site that depends on a single component set still gets the shipped defaults, but can only override them in Site Settings when it depends on fgtclb/academic-persons.

The content elements are hidden by default 

EXT:academic_persons hides all six of its content elements for the whole installation and brings them back per component. Whichever of the two mechanisms below you use, it is what makes an element selectable in the backend again — without one of them the content element is not offered, and existing records keep rendering.

Include the site set 

Add the set to the config.yaml of the site that should offer the content elements:

config/sites/my-site/config.yaml (diff)
 base: 'https://example.com/'
 rootPageId: 1
+dependencies:
+  - fgtclb/academic-persons
Copied!

See also TYPO3 Explained, Using a site set as dependency in a site.

Include static templates 

For an installation that still configures its frontend through sys_template records, the same files are registered as static templates and as selectable page TSconfig files.

Include static TypoScript 

Edit the sys_template record of the site root and add the entry to Include static (from extensions):

Entry Delivers
Academic Persons: All components (academic_persons) Every component this extension ships, in one entry. This is the entry to use.
Academic Persons: Shared plugin settings (academic_persons) The shared plugin.tx_academicpersons block on its own. This is what installations selected before version 2.4, then named Academic Persons Settings. The stored value did not change, so such a record keeps working untouched.
Academic Persons: Standalone page (academic_persons) Everything All components delivers, plus the page object of the standalone flavour. Do not select it on a site that has a site package of its own.

There is one further entry per component — Academic Persons: Profile list, Academic Persons: Profile list and detail, Academic Persons: Profile detail, Academic Persons: Profile card, Academic Persons: Selected profiles and Academic Persons: Selected contracts. They exist so that the static mechanism has the same shape as the sets. Because all six components share one TypoScript block, each of them delivers the same thing, and selecting more than one of them changes nothing.

Include static page TSconfig 

This is the half that decides which content elements the backend offers. Edit the page record of the site root, tab Resources, field Page TSconfig, and add the entries for the content elements the page tree should offer:

Entry Delivers
Academic Persons: All components (academic_persons) Every component this extension ships, in one entry.
Academic Persons: Profile list (academic_persons) Makes the Persons List content element selectable, and configures its entry in the new content element wizard.
Academic Persons: Profile list and detail (academic_persons) The same for Persons List and Detail.
Academic Persons: Profile detail (academic_persons) The same for Persons Detail.
Academic Persons: Profile card (academic_persons) The same for Contacts, plus the FlexForm restriction of that element.
Academic Persons: Selected profiles (academic_persons) The same for Profiles: Selected Profiles.
Academic Persons: Selected contracts (academic_persons) The same for Profiles: Selected Contracts.

The setting is inherited by every page below the one it is set on.

Do not combine both 

A site that uses the site set and the static template reads the shipped files twice. The site set is applied before the sys_template record, so the second read happens after the site settings and after config/sites/<site>/constants.typoscript — and it resets every constant the extension ships a default for back to that default.

Nothing else is damaged: the Constants and Setup fields of the sys_template record, the page TSconfig of a page and the page TSconfig files selected on a page are all applied afterwards and still win. Use one mechanism per site and the question does not arise.

General configuration 

Extension configuration There are some options for global extension configuration:

types.physicalAddressTypes

types.physicalAddressTypes
type

string

Default

private=Private,business=Business

The available types for physical addresses that can be chosen when adding a physical address to a profile.

types.emailAddressTypes

types.emailAddressTypes
type

string

Default

private=Private,business=Business

The available types for email addresses that can be chosen when adding an email address to a profile.

types.phoneNumberTypes

types.phoneNumberTypes
type

string

Default

private=Private,business=Business,mobile=Mobile

The available types for phone numbers that can be chosen when adding a phone number to a profile.

profile.feuser.telephoneNumberType

profile.feuser.telephoneNumberType
type

string

Default

business

The type assigned to telephone numbers imported from frontend users. The value must be one of types.phoneNumberTypes. An unavailable value is stored as the undefined type ''.

profile.feuser.faxNumberType

profile.feuser.faxNumberType
type

string

Default

business

The type assigned to fax numbers imported from frontend users. It is validated independently from profile.feuser.telephoneNumberType; an unavailable value is stored as the undefined type ''.

demand.allowedGroupByValues

demand.allowedGroupByValues
type

string

Default

firstNameAlpha=LLL:EXT:academic_persons/Resources/Private/Language/locallang_be.xlf:flexform.el.groupBy.items.first_name,lastNameAlpha=LLL:EXT:academic_persons/Resources/Private/Language/locallang_be.xlf:flexform.el.groupBy.items.last_name

What values are allowed to group person listings?

demand.allowedSortByValues

demand.allowedSortByValues
type

string

Default

firstNameAlpha=LLL:EXT:academic_persons/Resources/Private/Language/locallang_be.xlf:flexform.el.groupBy.items.first_name,lastNameAlpha=LLL:EXT:academic_persons/Resources/Private/Language/locallang_be.xlf:flexform.el.groupBy.items.last_name

What values are allowed to sort person listings?

Profile sections 

Configuration/AcademicPersons/Settings.yaml describes a profile in four top-level maps. It ships with academic_persons, which owns the records and their TCA, and it is the one file the backend record editor, the public detail view and the editing frontend of EXT:academic_persons_edit read.

Map Describes
profile The public detail layout ( structure and details ), and every directly editable profile property with its section, control and validators.
special The components of the editing frontend that are not one property: the composed display name, the image and the synchronisation switch.
contracts The contract fields, and the address, email and phone sections a contract owns.
documentSections The sortable lists attached to a profile: the seven timeline entry types and the contracts.

The order of every map and list is preserved and is what the editing frontend renders. The validator flags are documented on their own page.

The profile map 

Two keys describe the public layout, everything else is a field:

profile:
  structure:
    left:
      - menuSections
    right:
      - headline
      - position
      - profileImage
      - contact
      - subline
      - profileEntries
      - menuSectionsDatas
  details:
    headline:
      - title
      - firstName
      - middleName
      - lastName
    subline: 'LLL:EXT:academic_persons/Resources/Private/Language/locallang.xlf:detail.subline'
  gender:
    section: information
    fieldType: select
    renderType: select
    validators:
      - required
  firstName:
    section: information
    fieldType: input
    renderType: text
    validators:
      - readonly
      - disabled
    helptext: 'LLL:EXT:academic_persons/Resources/Private/Language/locallang.xlf:helptext.firstName'
  miscellaneous:
    section: aboutme
    fieldType: textarea
    renderType: ckeditor
    characterLimit: 1000
    validators:
      - html
Copied!
structure
The layout columns and the ordered elements in each column. The shipped detail template renders left as the desktop navigation and right as the main content; on mobile, the left elements are inserted again directly before subline.
details
Per element, the ordered profile properties, the relation map, the label reference or the special renderer it renders. Supported elements are menuSections , headline , position , profileImage , contact , subline , profileEntries and menuSectionsDatas ; an unknown element renders nothing. position and contact take the special renderer special: datasFromContracts . menuSections lists stable navigation identifiers and menuSectionsDatas maps each of them to the profile relation it shows.

How the layout is rendered 

The shipped Resources/Private/Templates/Profile/Detail.html receives the two keys as publicProfile and dispatches every identifier of structure to a partial of the same name below Resources/Private/Partials/Profile/PublicProfile/:

Element details entry Renders
menuSections Ordered navigation identifiers One link per identifier whose relation has records
headline Ordered profile properties The non-empty ones as the parts of the heading
position special: datasFromContracts The position of every contract
profileImage Ordered image properties Every non-empty one, as a figure
contact special: datasFromContracts Email addresses, phone numbers, postal addresses and location with room of every contract
subline A label reference The translated heading, and the point before which the left elements are repeated below the large breakpoint
profileEntries Ordered rich text properties The non-empty ones as fold-out entries
links Ordered link properties The non-empty ones, each with its companion title property ( website with websiteTitle) as the link text
menuSectionsDatas Navigation identifier to relation map One timeline section per identifier whose relation has records

Overriding a partial changes how an element renders, overriding profile changes what renders and where. The years of a timeline entry are printed as they are stored - a single year, a range, or an open range prefixed with a "since" or "till" label - and nothing about them is locale dependent.

Below the large breakpoint the elements of the left column are rendered a second time, directly before the subline element of the right column. A layout without subline in right therefore has no mobile navigation.

The view is a content element and renders no <main>, no <aside> and no <h1> - a page may carry two profile plugins, and those belong to the page template. Its headings start at <h2> for the headline, with the block headings one level below it.

The template loads the stylesheet Resources/Public/Css/frontend/profile-detail.css and the module @fgtclb/academic-persons/frontend/profile.js through the asset collector. The module toggles the fold-out entries, keeps the sticky navigation below a page header with the id page-header and, when the site loads Bootstrap, marks the section in view through its ScrollSpy. The icons of the contact rows and the fold-out entries are the identifiers academic-persons-envelope, academic-persons-phone, academic-persons-address, academic-persons-room, academic-persons-detail-plus and academic-persons-detail-minus of Configuration/Icons.php; a site package re-registers an identifier to replace the glyph. They are Bootstrap Icons, and their MIT licence ships beside them in Resources/Public/Icons/LICENSE-bootstrap-icons.txt.

The colours of the view are custom properties declared on the .academic-persons-detail root element - --academic-persons-detail-text, --academic-persons-detail-border and the two accents. Redeclaring them on that class in the site's own stylesheet is how the view is themed; the shipped stylesheet touches nothing outside that element.

What an override of the detail template loses 

Until 2.4 Templates/Profile/Detail.html was the view: it rendered the image, the contact data and every timeline section itself. Since 3.0.0 it is a dispatcher over the structure map, and the eleven partials below Partials/Profile/PublicProfile/ are what renders.

A project that overrides the template keeps rendering its own copy. Nothing looks broken, and two things are silently gone:

  • The configurable layout. profile.structure and profile.details are handed to the template as publicProfile and are read by the new partials only, so changing them has no effect at all while the old template renders.
  • Three partials the detail view no longer renders. Partials/Profile/Header.html and Partials/Profile/SectionHeader.html are still shipped and still rendered - by the list and card views, and by the profile editing view of academic_persons_edit - so an override of one of them made for the detail view no longer reaches it. Partials/Profile/DataHeader.html had the detail view as its only caller and is deleted: a project template that still renders Profile/DataHeader fails at render time rather than rendering nothing. Breaking: The partials of the detail view change has the migration.

Adopt the new template instead, and move the project's changes into the partial of the element they belong to: they are one file per element, and overriding one of them is what How the layout is rendered describes.

The fields 

Every other key is a field, and fields share one shape across profile , contracts.fields and contracts.contactSections.<section>.fields :

Key Meaning
section Profile fields only. Fields with the same section are grouped and rendered together, in file order; the first field of a section decides where the section appears.
propertyName The domain and form data property, when it differs from the key. Optional.
fieldName The database column, when it differs from the underscored property name. Optional.
fieldType input , select , textarea or check . Describes the frontend control; the TCA column keeps the type its TCA file declares.
renderType The renderer of the editing frontend: text , select , checkbox , email , phone , date , combinedLink or ckeditor .
validators The flag list.
characterLimit Rich text fields ( renderType: ckeditor ) only: the maximum number of readable characters. Checked on the server, never copied into the TCA.
helptext A label reference or literal text rendered next to the control.
autocomplete Contract and contact fields only: an HTML autocomplete token.
options Contract selects only: organisationalUnits , functionTypes or locations .

A field is dropped silently when it has no section (profile fields), no fieldType or no renderType . Removing a field from the file removes it from the editing frontend and from the validation; it never removes a column or stored data.

The special map 

special:
  title:
    type: special
    renderType: title
    fields:
      - title
      - firstName
      - middleName
      - lastName
  image:
    type: special
    renderType: cropper
  skipSync:
    type: special
    fieldType: check
    renderType: checkbox
Copied!

title composes the display name from the listed profile properties, image is the profile image and skipSync the switch that keeps a profile out of the synchronisation from its frontend user. A special entry with a fieldType and without composed fields addresses one profile column directly and takes part in the profile validation; the other two do not.

The contracts map 

contracts:
  label: 'LLL:EXT:academic_persons/Resources/Private/Language/locallang_tca.xlf:tx_academicpersons_domain_model_profile.columns.contracts.label'
  type: contracts
  fieldName: contracts
  rowFields:
    - position
  actions:
    - view
    - down
    - up
    - delete
    - edit
  fields:
    position:
      fieldType: input
      renderType: text
      validators:
        - required
    organisationalUnit:
      fieldType: select
      renderType: select
      options: organisationalUnits
    validFrom:
      fieldType: input
      renderType: date
      validators:
        - required
        - date
  contactSections:
    emailAddresses:
      fields:
        emailAddress:
          propertyName: email
          fieldName: email
          fieldType: input
          renderType: email
          autocomplete: email
          validators:
            - required
            - email
        emailAddressType:
          propertyName: type
          fieldName: type
          fieldType: select
          renderType: select
Copied!

fields are the contract fields in editor order. The three contact sections - physicalAddresses , emailAddresses and phoneNumbers - each carry their own fields map. Their keys are unique across the file, which is why emailAddress names the email property and column and each <section>Type key names the type property of its own record.

label , type , fieldName , rowFields and actions complete the contracts entry of the document sections below.

The document sections 

documentSections:
  contracts:
    type: contracts
  publications:
    label: 'LLL:EXT:academic_persons/Resources/Private/Language/locallang_tca.xlf:tx_academicpersons_domain_model_profile.columns.publications.label'
    type: publication
    fieldName: publications
    rowFields:
      - year
      - title
    actions:
      - view
      - down
      - up
      - delete
      - edit
    validators:
      title:
        - required
      link:
        - url
      from:
        - number
      to:
        - number
      year:
        - required
        - number
      description:
        editor:
          limit: 500
          type: ckeditor
Copied!

Each key is a stable section identifier; the map order is the display order.

Key Meaning
label The label reference of the section heading.
type The record type of the rows, i.e. the type of the profile information records. contracts is the reserved value for the contract section, which takes everything it does not declare from the top-level contracts map.
fieldName The profile relation the rows hang off.
readonly true disables creation and every mutating action; the section still offers view.
rowFields The values shown in a compact row, in order. Timeline entries support from, to, year, title and description; contracts support from, to and position.
actions The actions offered per row, in order: view, down, up, delete and edit. An action not listed is not available. Listing both up and down also enables drag sorting.
validators A map from field to flag list, or to a map with validators , individual <flag>: true entries and an editor block. editor.type: ckeditor implies the html flag and takes a readable-text limit ; editor.type: textarea implies textarea. The contract section validates against contracts.fields instead.
helptext A map from field to label reference.

The validators of a timeline section address the record type of that section only: a required title of publications does not make the title of a lecture required, neither in the editing frontend nor in the backend, where the flags land in the columnsOverrides of that record type. The field keys from, to and description are aliases of the yearStart, yearEnd and bodytext properties (columns year_start, year_end and bodytext); year addresses the year property and column.

Unknown row fields and actions are discarded, as are duplicates; both lists are matched without regard to case.

Overriding the file 

The file is collected from all installed extensions: every package that contains Configuration/AcademicPersons/Settings.yaml contributes, and the package loaded last wins. The files are merged on the top level only - a site package that defines profile replaces the shipped profile map completely, layout and fields alike, and the maps it does not mention stay as shipped. There is no deep merge and no syntax for changing a single flag of a single field.

To change the shipped configuration:

  1. Add Configuration/AcademicPersons/Settings.yaml to your site package.
  2. Make the site package depend on academic_persons in its composer.json or ext_emconf.php, so that it is loaded after it.
  3. Copy the complete map you want to change from EXT:academic_persons/Configuration/AcademicPersons/Settings.yaml and edit the copy.
  4. Flush the TYPO3 caches. The normalised graph is cached in the core cache.

There is no TypoScript and no site set equivalent for these settings.

Validation settings 

Every field of Configuration/AcademicPersons/Settings.yaml carries a list of flags that say whether it is required, read only or disabled, and what kind of value it takes. One list drives both editing contexts:

  • the TYPO3 backend record editor (FormEngine), through generated TCA, and
  • the editing frontend of EXT:academic_persons_edit.

This is why the file ships with academic_persons, which owns the records and their TCA, and not with the editing extension. The backend half applies even when the editing extension is not installed. Where the fields live - the profile , special , contracts and documentSections maps - is documented on the Profile sections page; this page is about the flags.

Where the flags are declared 

A profile, contract or contact field carries its flags as a list:

profile:
  gender:
    section: information
    fieldType: select
    renderType: select
    validators:
      - required
  firstName:
    section: information
    fieldType: input
    renderType: text
    validators:
      - readonly
      - disabled
Copied!

A document section carries a map from field to flag list, with an expanded map for a rich text field:

documentSections:
  publications:
    validators:
      title:
        - required
      year:
        - required
        - number
      link:
        - url
      description:
        editor:
          type: ckeditor
          limit: 500
Copied!

A field without flags is unconfigured: it is editable, not required, and no validator runs for it. A field that is not listed at all is not offered by the editing frontend.

Available flags 

Flag names are matched case insensitively. Anything not listed here is kept in the list and has no effect.

Flag Effect
required The field must not be empty. Adds a not empty validation in the frontend and marks the field required in the backend.
disabled The field must not be edited at all. See the note below.
readonly The field is shown but cannot be written.
email The value must be a valid email address; the field is rendered as an email input and the TCA column becomes an email column.
url The value must be a valid URL; the field is rendered as a URL input. The TCA column is untouched.
number The field is rendered as a number input and the TCA column becomes a number column. No additional server side validation is performed.
date The field is rendered as a date input. The TCA column keeps its own datetime configuration.
tel The field is rendered as a telephone input. No phone number format is enforced, and the TCA column is untouched.
textarea The field is rendered as a multi line text control. The TCA column is untouched.
html The field is rich text: the editing frontend renders the rich text editor and sanitises the submitted markup. The TCA column is untouched.

Only required , email and url run a validator on the server; the other flags select the control and the input normalisation. Validator class names and validator options cannot be put in the list.

Character limits 

A rich text field may limit the number of readable characters - markup is not counted. A profile or contract field declares it next to its render type, a document field in its editor block:

profile:
  miscellaneous:
    section: aboutme
    fieldType: textarea
    renderType: ckeditor
    characterLimit: 1000
    validators:
      - html

documentSections:
  publications:
    validators:
      description:
        editor:
          type: ckeditor
          limit: 500
Copied!

The limit is effective only on a ckeditor control; on any other control it is ignored. It is checked on the server and shown by the editing frontend; it is never copied into the TCA, because FormEngine's max would count the markup.

Fields that are locked by default 

The three name fields ship as readonly and disabled :

profile:
  firstName:
    validators:
      - readonly
      - disabled
  middleName:
    validators:
      - readonly
      - disabled
  lastName:
    validators:
      - readonly
      - disabled
Copied!

This is intentional. Profile names are usually owned by the connected frontend user record - commonly fed from a directory service such as LDAP or Active Directory, and synchronised into the profile - so they must not be overwritten from an editing form.

The consequences, which surprise people who did not expect them:

  • First name, Middle name and Last name are read only in the backend record editor, for every backend user.
  • The same three fields are rendered locked in the editing frontend, and a value submitted for them is discarded on the server.

If the profile names are maintained in TYPO3 rather than synchronised from elsewhere, remove the two flags as described below.

Effects in the TYPO3 backend 

The flags of every section are merged into the TCA of the matching table, so a locked field is read only in the record editor and a required field is marked as such:

Section Table
profile fields and special.skipSync tx_academicpersons_domain_model_profile
contracts.fields tx_academicpersons_domain_model_contract
contracts.contactSections.emailAddresses tx_academicpersons_domain_model_email
contracts.contactSections.phoneNumbers tx_academicpersons_domain_model_phone_number
contracts.contactSections.physicalAddresses tx_academicpersons_domain_model_address
every other documentSections entry tx_academicpersons_domain_model_profile_information , as columnsOverrides of the record type of that section

The property name is translated to the database column automatically: firstName addresses first_name ; a field that names a fieldName addresses that column instead.

The seven timeline sections share one table, so their flags apply to their record type only: a required title of publications does not make the title of a lecture required. The fieldType and renderType of a field never reach the TCA - the column keeps the type its TCA file declares.

Effects in the editing frontend 

When EXT:academic_persons_edit is installed, the same flags are used three times:

  1. The control is rendered with the matching disabled , readonly and required attributes and the input type the flags select.
  2. required , email and url add server side validation of the submitted data, and a character limit is enforced.
  3. A disabled or readonly property is never written to the record, whatever the request contains. This is deliberate: it protects already stored data, and it is what prevents a locked field from being emptied when a form is submitted.

Validation never falls back from one section to another: a contact record is validated against its contact section, a timeline entry against the section of its record type, and the profile against its profile sections.

Overriding the flags 

The flags live in the map that carries the field, so changing them means overriding that map - see Overriding the file. The files are merged on the top level only: a site package that defines profile replaces the shipped profile map completely, layout and fields alike, and there is no syntax for changing a single flag of a single field.

Example - making the profile names editable again, in the backend and in the editing frontend. The shipped profile map is repeated with the two flags removed from the three name fields; the layout keys and the other fields are copied unchanged and are shortened here for readability:

profile:
  structure:
    # ... as shipped
  details:
    # ... as shipped
  gender:
    section: information
    fieldType: select
    renderType: select
    validators:
      - required
  firstName:
    section: information
    fieldType: input
    renderType: text
  middleName:
    section: information
    fieldType: input
    renderType: text
  lastName:
    section: information
    fieldType: input
    renderType: text
  # ... the remaining fields as shipped
Copied!

There is no TypoScript and no site set equivalent for these settings.

Migrating a pre-3.0 override 

Before 3.0 the file had two top-level maps, validations with one flag list per record type and profileInformationsTypes with the seven timeline entry types, and the manual told integrators to restate the complete validations block in the site package. Such a file keeps working after the update: the two keys are mapped onto the section maps at runtime, before the settings graph is built, and a warning naming the package and the key is logged once per cache build. The mapping is transitional and is removed in academic_persons 4.0, so the override should be rewritten.

The console command prints the replacement:

vendor/bin/typo3 academic:persons:settings:migrate
Copied!

For every active package that still ships a legacy key it prints the package, the keys it found, one comment line per entry that could not be mapped, and the four maps profile , special , contracts and documentSections as the runtime mapping produces them - the complete document that replaces the legacy keys in that package's file. It exits with 1 when such a package exists and with 0 otherwise, so a deployment pipeline can run it as a check. The command never writes the file: the override lives in a site package that is under version control and usually deployed read only, so the printed maps are pasted into the file after review, and the TYPO3 caches are flushed afterwards. When EXT:reports is installed, the Status report lists the same packages under Academic Persons as a warning.

How the legacy keys map:

Legacy key Mapped onto
validations.profile.<property> profile.<field>.validators
validations.contract.<property> contracts.fields.<field>.validators
validations.emailAddress.{email, type} contracts.contactSections.emailAddresses.fields.{emailAddress, emailAddressType}.validators
validations.phoneNumber.{phoneNumber, type} contracts.contactSections.phoneNumbers.fields.{phoneNumber, phoneNumberType}.validators
validations.physicalAddress.<property> contracts.contactSections.physicalAddresses.fields.<field>.validators , type onto physicalAddressType
validations.profileInformation.<property> documentSections.<section>.validators.<field> of every timeline section; yearStart , yearEnd and bodytext onto from , to and description
profileInformationsTypes.<section> the label of documentSections.<section> ; its type and fieldName are reported, not applied

A field is matched by its key or by the property it names, so emailAddress.email reaches the emailAddress field whose propertyName is email . A legacy set decides the five flags the old shape knew - required , readonly , disabled , email and number - for every field of its target: a field the set does not list has none of them, exactly as an unlisted property was unconfigured before, which is what made the 2.x example above unlock the profile names by not listing them. The flags the old shape could not express - url , date , tel , textarea , html - stay as the section maps declare them.

Two things are not mapped and are reported by the command and in the log:

  • A property the section maps do not know, and an eighth timeline entry type declared under profileInformationsTypes , are skipped. The type needs a profile relation and a TCA column the settings never created; the Breaking entry on the section based settings describes how to keep one.
  • The type and fieldName of a timeline entry type are not applied. Until 2.4 the two generated the inline column of the profile table, so overriding one moved the backend relation and the frontend selection together; since 3.0.0 the seven relations are declared by the TCA of the profile table, and applying the override would move the frontend half alone - records created in the editing frontend would be invisible in the backend, and the other way round. The section keeps the record type and the relation field that match the TCA, and the value the override named is printed as a note. Act on that note: a timeline type of your own needs its own column in a TCA override of the profile table, as The document sections and the Upgrading from 2.4 to 3.0.0 page describe.

Route enhancers 

This extension ships three ready made route enhancers below Configuration/Routes/. TYPO3 does not read those files on its own — they are fragments that have to be imported from the configuration of the site which shows the plugins.

Each file covers exactly one plugin, so which of them you import follows from which plugins the site actually uses.

What the files enhance 

Detail.yaml
Enhancer ProfileDetailPlugin for the plugin Detail , argument namespace tx_academicpersons_detail . One route, /{profile_name} , for the detail action, mapping the argument profile . The path segment is resolved by a PersistedAliasMapper on the table tx_academicpersons_domain_model_profile over the field slug .
List.yaml
Enhancer ProfileListPlugin for the plugin List , argument namespace tx_academicpersons_list . Two routes for the list action: {localized_page}-{page} for demand/currentPage and /{letter} for demand/alphabetFilter . The page number is limited to a StaticRangeMapper from 1 to 1000, the letter to a StaticRangeMapper from a to z, and the word in front of the page number is translated by a LocaleModifier page by default, seite for German.
ListAndDetail.yaml
Enhancer ProfileListAndDetailPlugin for the plugin ListAndDetail , argument namespace tx_academicpersons_listanddetail . It is the union of the two above: the detail route, the pagination route and the letter route, with the same aspects, because that plugin renders both the list and the detail view.

Which file to import 

Which of them you import follows from which plugins the site actually uses:

  • A site that puts the List plugin on one page and the Detail plugin on another — the usual setup, where the list links to the detail page through the plugin setting detailPid — imports List.yaml and Detail.yaml.
  • A site that puts the single ListAndDetail plugin on one page imports ListAndDetail.yaml only.
  • A site that uses both variants imports all three, and then has to bound each of them to its own pages — see the next section.

The remaining plugins of this extension — SelectedProfiles , SelectedContracts and Card — take no frontend arguments, so no enhancer is shipped for them.

Limiting an enhancer to its pages 

An enhancer is offered to every page of the site unless it says otherwise, and TYPO3 takes the first candidate route whose path matches and whose aspects resolve. Two enhancers are not kept apart by belonging to different plugins, nor by carrying different keys — neither is what the matcher looks at.

The three files of this extension describe the same two views, so their routes overlap by construction:

Route Declared in
/{profile_name} Detail.yaml and ListAndDetail.yaml
{localized_page}-{page} List.yaml and ListAndDetail.yaml
/{letter} List.yaml and ListAndDetail.yaml

Each pair is identical down to the mapper, so importing more than one file without saying where it applies means the file imported first takes those URLs on every page of the site. The plugin on the other page then never receives its argument: the dedicated detail page answers 404, and the combined plugin renders the unfiltered list where a letter was asked for.

Only resolving is ambiguous. Generating a URL is scoped to the plugin namespace being linked, so the links keep looking right, which is why this surfaces as a broken page rather than as a broken link.

limitToPages is the answer, and with it in place the import order no longer matters:

config/sites/my_site/config.yaml
imports:
  - resource: 'EXT:academic_persons/Configuration/Routes/List.yaml'
  - resource: 'EXT:academic_persons/Configuration/Routes/ListAndDetail.yaml'
  - resource: 'EXT:academic_persons/Configuration/Routes/Detail.yaml'

routeEnhancers:
  ProfileListPlugin:
    limitToPages: [12, 13]
  ProfileListAndDetailPlugin:
    limitToPages: [14]
  ProfileDetailPlugin:
    limitToPages: [15]
Copied!

The uids are those of the pages carrying the plugin in question, and they are the uids of the default language: matching derives the page as l10n_parent ?: uid , so one list covers every translation of that page. Plain page uids work on every TYPO3 version this extension supports.

A site that imports a single file needs no limitation for this extension, but adding it is still worth the two lines. What keeps a route path of the same shape from another extension apart is only that its mapper rejects the value — a slug that happens to exist in both tables is enough to make the two compete.

What the URLs look like 

Assuming the list plugin sits on a page with the slug /persons and the detail plugin on /persons/profile, the URLs change as follows.

Without the enhancers
/persons?tx_academicpersons_list%5Bdemand%5D%5BcurrentPage%5D=2
/persons?tx_academicpersons_list%5Bdemand%5D%5BalphabetFilter%5D=m
/persons/profile?tx_academicpersons_detail%5Bprofile%5D=42
Copied!
With the enhancers imported
/persons/page-2
/persons/m
/persons/profile/jane-doe
Copied!

Caveats 

  • The detail route needs a slug. PersistedAliasMapper resolves the path segment against the slug field of the profile record, so a profile whose slug is empty cannot be reached through the enhanced URL. The slug is generated by the TCA slug field, which means it is filled when the record is saved in the backend. Profiles created by academicpersons:createprofiles are persisted through the Extbase persistence manager and therefore never pass the DataHandler , so those records — and records that predate the field — start out with an empty slug and have to be saved once in the backend before the enhanced URL resolves.
  • The two list routes are alternatives, not a combination. A link that carries a page number and a letter matches the pagination route, and the letter stays behind as a query argument — /persons/page-2?tx_academicpersons_list[demand][alphabetFilter]=m.
  • Only the mapped value ranges are put into the path. A page number above 1000, and the empty filter value that the A-Z reset link of the alphabet pagination submits, are outside the mapped ranges, so those links keep their query argument.
  • The localeMap of the LocaleModifier is matched against the locale of the site language, with the underscores replaced by hyphens and anchored at the start. The shipped map lists en_EN.* and de_DE.* , which means a German language configured as de-DE is translated to seite while a plain de is not. Adjust the map to the locales your site actually uses.
  • Unlike the program list of academic_programs, the pagination and the alphabet filter of this extension are rendered as links, not as a form, so their own requests do carry the arguments in the URL and are enhanced.

Overriding templates 

EXT:academic_persons is using Fluid as template engine.

This documentation won't bring you all information about Fluid but only the most important things you need for using it. You can get more information in the section Fluid templates of the Sitepackage tutorial. A complete reference of Fluid ViewHelpers provided by TYPO3 can be found in the ViewHelper Reference

Change the templates using TypoScript constants 

As any Extbase based extension, you can find the templates in the directory Resources/Private/.

If you want to change a template, copy the desired files to the directory where you store the templates.

We suggest that you use a sitepackage extension. Learn how to Create a sitepackage extension.

# TypoScript constants
plugin.tx_academicpersons {
    view {
        templateRootPath = EXT:mysitepackage/Resources/Private/Extensions/myextension/Templates/
        partialRootPath = EXT:mysitepackage/Resources/Private/Extensions/myextension/Partials/
        layoutRootPath = EXT:mysitepackage/Resources/Private/Extensions/myextension/Layouts/
    }
}
Copied!

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.

The trigger: 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. This extension dispatches it after a profile is auto-created for a frontend user ( AbstractProfileFactory::createProfileForUser() , also reached by the academic:createprofiles command); EXT:academic_persons_edit dispatches it after every persisting frontend edit action, and project code — typically a DataHandler 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 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 profile's pid must resolve to a site — the synchronisation listener of EXT:academic_persons_edit determines the site from the request or from the pid and skips the event silently when it cannot.

Updating an existing profile from its frontend user record ( AbstractProfileFactory::updateProfileForUser() , command academic:updateprofiles ) 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_sync flag is set is neither updated nor announced.

The synchronisation surface 

\FGTCLB\AcademicPersons\Service\RecordSynchronizerInterface declares one method, synchronize(SynchronizerContext $context) . The shipped implementation ( RecordSynchronizer ) routes every write through the TYPO3 DataHandler — nothing in TYPO3 outside the DataHandler honours l10n_mode=exclude 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\AcademicPersons\Domain\Model\Dto\Syncronizer\SynchronizerContext describes one synchronisation run. Build it through SynchronizerContext::create() , 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 localize command — the full record, including its inline child tree;
  • for an existing translation, re-submits the default record's l10n_mode=exclude column values as a datamap (core's DataMapProcessor propagates them into every translation) and issues an inlineLocalizeSynchronize command 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\AcademicPersons\Event\ModifyProfileImageMetadataEvent is dispatched immediately before this extension writes the metadata of a profile image, and a listener decides what is written: whatever it leaves in getMetadata() 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 getTargetTable() says which one:

getTargetTable() Written when Fields
sys_file_metadata 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_file_reference 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. getFile() is the file — its own metadata record is $event->getFile()->getMetaData() — and getFileReference() is the image relation of the profile, null only for a profile that has none. getProfileUid() is the profile record the image belongs to, a translation for an image of its own, and getRequest() 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_file_metadata and typo3/cms-filemetadata , and the relation row has no equivalent.

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.

EXT:my_extension/Classes/EventListener/AddImageRightOfUse.php
<?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);
    }
}
Copied!

See also 

  • The changelog entry Translation sync is routed through the DataHandler for the behavioural differences to versions before 3.0.
  • EXT:academic_persons_edit, whose profile.allowedLanguages setting feeds the allowed language ids and whose event listener wires the pieces together.

Known problems 

Please note that this extension is still in development. Changes to existing code may appear in upcoming versions.

If you run into a bug or a feature that would be helpful, please use the issue tracker.

ChangeLog v3 

Every change to the Academic Persons extension is documented here.

Also available 

3.0 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: Person records are workspace aware 

Description 

All nine record tables of this extension now declare 'versioningWS' => true in their TCA ctrl section:

  • tx_academicpersons_domain_model_profile
  • tx_academicpersons_domain_model_profile_information
  • tx_academicpersons_domain_model_contract
  • tx_academicpersons_domain_model_address
  • tx_academicpersons_domain_model_email
  • tx_academicpersons_domain_model_phone_number
  • tx_academicpersons_domain_model_organisational_unit
  • tx_academicpersons_domain_model_function_type
  • tx_academicpersons_domain_model_location

None of them did before, so none of the records of this extension could be created or changed in a workspace, and the workspaces module did not offer them.

Nothing reported the gap. The automatic TCA migration TYPO3 v14 ships for this, TcaMigration::addWorkspaceAwarenessToInlineChildren() , repairs an inline child only when its parent table is already declared workspace aware. Here the inline parents — profile for contracts and profile information, contract for addresses, email addresses and phone numbers, and organisational_unit as a second parent of contract — were unflagged themselves, so it never fired and no deprecation was logged. TYPO3 v13 carries no such migration at all.

function_type and location are plain select targets of contract rather than inline children. They are flagged for consistency, so that an editor can add a function type or a location as part of the same draft that uses it.

Impact 

The database schema changes. \TYPO3\CMS\Core\Database\Schema\DefaultTcaSchema derives the t3ver_oid , t3ver_wsid , t3ver_state and t3ver_stage columns and an index over the first two from the declaration, so every one of the nine tables needs those columns added.

This is not optional and it does not wait for someone to open a workspace. A workspace aware table is queried with a WorkspaceRestriction in the live workspace too, so until the database analyzer has run, both of these raise a database error about the unknown columns:

  • the backend record lists of this extension — DatabaseRecordList adds the restriction unconditionally, with the backend user's workspace, including workspace 0 ;
  • the frontend rendering of any translated profile — the language overlay in \TYPO3\CMS\Core\Domain\Repository\PageRepository selects the overlay record with a FrontendRestrictionContainer , which carries the restriction by default.

Editing changes permanently, and running the analyzer does not undo it. In a workspace with live editing enabled, a profile edit previously went straight to live, because TYPO3 permits live editing only for tables that are not workspace aware. It now becomes a workspace version that has to be published.

Custom queries against these tables have to be adapted. A plain QueryBuilder selecting from any of the nine tables now sees workspace versions as ordinary rows and, without a version overlay, will render unpublished drafts into the live frontend. Code that goes through the Extbase repositories of this extension is not affected: Extbase adds the constraint and performs the overlay itself, on TYPO3 v13 and v14 alike.

Affected Installations 

Every installation of this extension, and every installation of academic_contacts4pages , whose contact records hang below tx_academicpersons_domain_model_contract as inline children.

No existing record is touched and no rendered output changes — but the database analyzer has to run, and until it does the two places named under Impact are broken. Development instances built from a committed database snapshot need the same treatment.

Projects and extensions that query the nine tables with their own QueryBuilder are affected regardless of whether they use workspaces today, because a workspace version created later becomes visible to them.

Migration 

Run the database analyzer once after updating, in the Admin Tools > Maintenance module or with vendor/bin/typo3 extension:setup .

For custom queries, add the restriction and the overlay:

$queryBuilder->getRestrictions()->add(
    GeneralUtility::makeInstance(
        WorkspaceRestriction::class,
        (int)$context->getPropertyFromAspect('workspace', 'id', 0),
    ),
);

// ... and per fetched row, before using it:
$pageRepository->versionOL($table, $row, true);
if (!is_array($row)) {
    continue;
}
Copied!

Note that versionOL() keeps the live uid of an overlaid record, so relations resolved through it — the profiles of a frontend user through tx_academicpersons_feuser_mm , for instance — still read the live relation. fe_users is not workspace aware in TYPO3 itself. A relation changed inside a workspace is therefore not part of the preview.

Breaking: Extbase plugins require CType on TYPO3 v14 

Description 

TYPO3 v14 removed the tt_content sub-type feature (the list_type column) and changed ExtensionManagementUtility::addPlugin() accordingly. The academic plugins have been registered as first-class content elements (CType) since the 2.0 version line (see the 2.0 breaking note about migrating from list_type to CType); for TYPO3 v14 support the internal registration was adapted to the new addPlugin() signature and the vestigial list_type handling was dropped.

Impact 

On TYPO3 v14 the tt_content.list_type column no longer exists. Any content records still stored as CType=list with a list_type of one of the plugins below will no longer resolve, and custom TypoScript, TSconfig, page TSconfig or SQL that references list_type for these plugins stops working.

The change relates to the following plugins:

  • academicpersons_card
  • academicpersons_detail
  • academicpersons_list
  • academicpersons_listanddetail
  • academicpersons_selectedcontracts
  • academicpersons_selectedprofiles

Affected Installations 

Installations that upgrade to TYPO3 v14 and still hold content elements stored as CType=list + list_type=<plugin>, or that reference list_type for these plugins in their own configuration.

Migration 

Run the provided upgrade wizard academicPersons_MigrateListTypeToCTypeContentElements before upgrading to TYPO3 v14 (it requires the list_type column, which v14 removes) to migrate the tt_content records to the dedicated CType values. Update any custom configuration referencing list_type to match on CType instead.

Breaking: The profile image translates 

Description 

The image column of tx_academicpersons_domain_model_profile was declared 'l10n_mode' => 'exclude' with 'l10n_display' => 'defaultAsReadonly' : every translation of a profile carried the default-language image, the translation form showed it read-only, and there was no way to give one language a picture of its own.

The column now declares 'behaviour' => ['allowLanguageSynchronization' => true] instead. A translation starts out following the default-language image — the core's parent localization state, which is also what every existing translation is in, because no l10n_state has been stored for the column yet — and switches to the custom state as soon as it gets an image of its own. In the parent state an image the default-language profile gains or changes reaches the translation on the next write of the default record through the TYPO3 DataHandler — a backend save and the translation synchronisation of this extension both are one — and a removal reaches it through the DataHandler's own delete cascade, which deletes the localizations of a deleted reference. In the custom state the translation keeps its image whatever happens to the default one.

The relation itself is written by one service, \FGTCLB\AcademicPersons\Service\ProfileImageRelationWriter ( @internal ), which drives every change through the DataHandler and maintains the localization state. The frontend profile editing of EXT:academic_persons_edit uses that service as well from 3.0.0 on - its editor replacement of the same release no longer writes the image relation through Extbase - so an image uploaded there for a translation reaches the custom state exactly as a backend upload does.

The title and alternative text of the image reference follow the name of the profile record the reference belongs to: a backend save, a localization and a frontend profile update all rewrite the title and alternative of the profile's own sys_file_reference row from that record's title and names. Only the reference row is written; the sys_file_metadata row of the file, which a file may share between the languages of a profile, stays the backend editor's. The one write of that record is the frontend upload that creates the file, which fills its empty required attributes once — see the changelog entry Important: An uploaded profile image carries its metadata of fgtclb/academic-persons-edit .

Impact 

  • The backend translation form shows the image field editable, with the language synchronization toggle the core renders for such columns. An editor can keep a translation in sync with the default language or give it an image of its own.
  • The translation synchronisation of this extension (see Important: Translation sync covers child exclude columns) needs no change for it: the same core pass that propagates the exclude columns honours the localization state of the image.
  • The image reference of a profile carries the profile's name as title and alternative text after the next save of the record — from a backend save, a localization, or a profile update announced through AfterProfileUpdateEvent . A title or alternative text maintained on the reference row by hand is overwritten then; text maintained on the file's metadata is never touched by it.
  • Workspaces are unchanged for an edit: the writer addresses live records and lets the DataHandler produce the versioned rows, so a draft edit of a profile image stays in its workspace. An installation-wide repair is the exception and says so: the repair wizard of fgtclb/academic-persons-edit skips workspace rows and acts in the live workspace regardless of the workspace the person running it has selected.
  • Code that read the image of a translation through the default-language record — because the translation could never differ — has to resolve the translation's own reference now.

Affected Installations 

Every installation with translated profiles. Existing data needs no change: translations without a stored localization state follow the default-language image exactly as before.

Installations that uploaded profile images through academic_persons_edit before 3.0 should run the upgrade wizard academicPersonsEdit_repairLocalizedProfileImages of that extension, which brings the relation rows the old upload and synchronisation paths wrote into the shape the localization state expects.

Breaking: The partials of the detail view change 

Description 

The public profile detail view is assembled from configuration instead of a fixed template (Feature: Configurable public profile), and Resources/Private/Templates/Profile/Detail.html is rewritten for it. It dispatches every configured element to one partial below Resources/Private/Partials/Profile/PublicProfile/ and renders nothing by itself:

Contact.html, Element.html, Headline.html, Links.html, MenuSections.html, MenuSectionsDatas.html, Position.html, ProfileEntries.html, ProfileImage.html, Subline.html and TimelineItem.html.

Those eleven files are the override surface of the detail view from 3.0 on.

Three partials the previous Detail.html rendered are no longer part of it:

  • Partials/Profile/DataHeader.html is deleted. The detail view was its only caller. It rendered the heading of a data block through {header -> f:format.raw()} and a f:switch on a layout value; the configurable view gives every element its own heading, in its own partial, escaped.
  • Partials/Profile/Header.html and Partials/Profile/SectionHeader.html are kept, and the detail view no longer renders them. They still render the headings of the list and card views of this extension and of the contact plugins of EXT:academic_contacts4pages, so an override of either keeps working there and stops having any effect on a profile detail page.

Impact 

A project that overrode one of the three partials to style the detail view loses that styling: the deleted one is not read any more, and the two that stay are not reached from the detail view. Neither shows an error - the page renders, without the override.

A project that overrides Templates/Profile/Detail.html itself keeps rendering its own template and is affected differently: see Feature: Configurable public profile.

Affected Installations 

Every installation with a project override of Partials/Profile/DataHeader.html, Partials/Profile/Header.html or Partials/Profile/SectionHeader.html, and every project whose own template renders Profile/DataHeader.

Migration 

  1. Remove an override of Partials/Profile/DataHeader.html, and any <f:render partial="Profile/DataHeader" /> in project templates. A partial that no longer exists is a render time error, not a silent empty string.
  2. Move detail view styling out of Partials/Profile/Header.html and Partials/Profile/SectionHeader.html into the PublicProfile/ partial of the element it belongs to. Keep the override for the list and card views if it is still wanted there.
  3. Flush the TYPO3 caches, so the Fluid template cache is rebuilt.

Breaking: Removed TYPO3 v12 support 

Description 

Support for TYPO3 v12 has been removed for the 3.x version line, based on the dual TYPO3 core version support per major version of the academic extensions support matrix.

This includes removing build, test and configuration parts only required for TYPO3 v12. Version specific code paths are dropped in a dedicated step.

Impact 

TYPO3 v12 or older instances can no longer install or update to the 3.x version of the academic extensions and are required to upgrade TYPO3 first.

The extension cannot be installed on TYPO3 v12 anymore but does not break otherwise.

Affected installations 

All installations using an academic extension on TYPO3 v12 that want to upgrade to the 3.x version line.

Migration 

Upgrade the TYPO3 installation to a supported version - TYPO3 v13 or v14 - beforehand or within the same upgrade step.

Breaking: Section-based AcademicPersons settings 

Description 

Configuration/AcademicPersons/Settings.yaml changes shape. The flat schema of two top-level maps - profileInformationsTypes listing the seven timeline entry types, and validations with one flag list per record type - could say which fields are required or locked, and nothing else. The editing frontend of fgtclb/academic-persons-edit needs the order of the fields, the control each one is rendered with, its help text, the rows and actions of a sortable list, and the character limit of a rich text field. All of that is now declared in one place, in four top-level maps: profile , special , contracts and documentSections . The Profile sections page documents the shape, the Validation settings page the flags.

There is still one file, one factory and one cache entry. The public detail layout - structure and details - lives in the same profile map as the editable fields, so an override of the layout restates the fields with it. The backend TCA does consume the graph: five TCA files of this extension merge the validation set of their own section, exactly as they merged the flat sets before, and the sixth - the profile information table, one table shared by the seven timeline types - merges a types fragment so a section's flags land in the columnsOverrides of its own record type. The normalised graph is cached in the core cache under AcademicPersons_Settings_v3, the identifier the move of the validation primitives to EXT:academic_base introduced in the same release; it is not changed a second time.

What an integrator sees:

  • profileInformationsTypes is gone. The seven relations of a profile to its timeline entries ( scientific_research , vita , memberships , cooperation , publications , lectures , press_media ) are part of the domain model and are declared by the TCA file of the profile table. They used to be generated from the map, so an override that dropped an entry silently lost a backend column; they now exist whatever the settings say. The same seven appear as documentSections , which carry their label, record type, relation field, row fields, actions and validators.

    The record type and the relation field of a timeline type are no longer configurable. They used to be one value each, generating the backend column and selecting the frontend records together; since the column is TCA, a changed type or fieldName would move the frontend half alone and every record created through it would be invisible in the backend. An override of either is therefore not applied, and the legacy mapping reports it per key instead of copying it, see Feature: Legacy settings overlay and migration command. A timeline type of a project's own needs its own column in a TCA override of the profile table, and a documentSections entry that names it.

  • validations is gone. The flags of a field are declared on the field: profile.<field>.validators for the profile, contracts.fields.<field>.validators for the contract, contracts.contactSections.<section>.fields.<field>.validators for the address, email and phone records, and documentSections.<section>.validators.<field> for the timeline entries. Every set keeps to its own section: a timeline section reaches the backend as columnsOverrides of its record type, never as a column configuration all seven types share.
  • The flag vocabulary grows by url , tel , textarea and html , and a rich text field can carry a characterLimit . fieldType and renderType describe the frontend control only; the TCA column keeps the type its TCA file declares.
  • The shipped flags change, and both editing contexts apply that. Newly required: the profile's gender - the profile TCA column gets required and minitems, so the backend record editor refuses to save a profile without a gender, and the editing frontend runs its not empty validation - and the contract's validFrom . Newly validated: website , publicationsLink and the link of six of the seven timeline sections carry the url flag - cooperation offers no link field and therefore no flag on one. Relaxed: the type of an email address or phone number is no longer required, and the address's streetNumber and zip lose the number flag - their columns return from the number TCA type the flag set to the input their TCA file declares, so a street number like 12a is stored as entered instead of being cast to 12. Unchanged: the address's country , and the title and year of every timeline entry were required before.
  • The timeline entry validators address the year columns: year is the record's year, and the keys from and to alias its yearStart and yearEnd properties.
  • Every field of the shipped file carries a helptext , and the label file of academic_persons gains the 38 helptext.* units they reference - the help of the profile, contract, contact and timeline fields, rendered by the editing frontend. A project replaces the text either by pointing helptext at an LLL: key of its own (or at literal text), or by overriding the shipped unit through locallangXMLOverride .

The internal PHP API changes with the file. AcademicPersonsSettings exposes the graph - profileSections , specialFields , contractFields , contractContactSections , documentSections and publicProfile - built from the new value objects ProfileSection , ProfileField , SpecialField , ContractField , ContractContactSection , ContractContactField , DocumentSection and PublicProfileSettings , and answers validation questions per section. Removed without replacement:

Removed Instead
\FGTCLB\AcademicPersons\Settings\ProfileInformationType DocumentSection , resolved through AcademicPersonsSettings::getDocumentSection() or getDocumentSectionByType()
AcademicPersonsSettings::getProfileInformationType() getDocumentSection()
AcademicPersonsSettings::getValidationSet() , getValidationSetWithFallback() getProfileValidationSet() , getProfileUpdateValidationSet() , getContractContactValidationSet() and getDocumentValidationSet() , plus getProfileValidationSetForFields() and getContractContactValidationSetForFields() for a subset of a section's fields - every one of them returns an empty set for an unknown identifier, there is no separate fallback method
AcademicPersonsSettings::$profileInformationTypes , $validations the graph properties above

All of it is @internal and was consumed by EXT:academic_persons_edit only, which is adapted.

Impact 

Every site package that overrides the file has to be migrated. The old maps are not read as such any more. A file that still declares validations or profileInformationsTypes is mapped onto the section maps at runtime with a logged warning, until 4.0 - see the Feature entry on the legacy settings overlay and the migration command, and the migration section of the validation settings page. Without that mapping the installation would run on the shipped defaults: locked name fields, the required contact fields, and the seven shipped timeline sections.

An eighth timeline entry type that an override declared under profileInformationsTypes used to get a backend inline column for free. It no longer does: the profile relations are fixed. Such a type can be kept by declaring the column in a TCA override of the profile table and its section under documentSections - the TCA file's loop over the seven relations is the template for the column. The Extbase model of this extension never had a property for an additional type, so it was reachable in the backend only.

Code that reads the removed methods or the removed class fails with an undefined method or a class not found error.

Existing profiles without a gender can no longer be saved in the backend until a gender is chosen, and a frontend profile form that does not post one is rejected. An installation that does not want the gender required removes the flag in its override. Street numbers and zip codes accept non-numeric values again; values already cast to integers stay as they are.

Affected Installations 

Every installation with a site package shipping Configuration/AcademicPersons/Settings.yaml, and every installation whose project code reads AcademicPersonsSettings directly.

Migration 

  1. Copy the shipped EXT:academic_persons/Configuration/AcademicPersons/Settings.yaml over the override in the site package, and re-apply the project's changes to it: a locked or unlocked profile field is its validators list under profile , a required contact field its list under contracts.contactSections , a required timeline field its entry under the validators map of every section it applies to.
  2. Keep every map the override declares complete. The files are merged on the top level only, so a profile map in the override replaces the shipped one - the layout keys and every field included.
  3. Decide on the changed defaults: drop required from profile.gender.validators if profiles without a gender are to stay saveable, restore number on streetNumber and zip if numeric values are to be enforced, and add required back to the two <section>Type fields if a contact type is mandatory.
  4. Re-declare a renamed timeline relation in TCA. An override that changed profileInformationsTypes.<type>.type or ...fieldName no longer reaches the profile table, and the value it named is reported by vendor/bin/typo3 academic:persons:settings:migrate rather than applied. Either accept the shipped record type and relation, or declare the column in a TCA override of tx_academicpersons_domain_model_profile and give it a documentSections entry.
  5. Flush all TYPO3 caches.

Feature: Configurable document rows and actions 

Description 

Every entry below documentSections in Configuration/AcademicPersons/Settings.yaml - the seven timeline entry types and the contracts - declares what a compact row of that list shows and which actions it offers:

documentSections:
  publications:
    label: 'LLL:EXT:academic_persons/Resources/Private/Language/locallang_tca.xlf:tx_academicpersons_domain_model_profile.columns.publications.label'
    type: publication
    fieldName: publications
    rowFields:
      - year
      - title
    actions:
      - hide
      - view
      - down
      - up
      - delete
      - edit
Copied!

rowFields is the ordered list of values a row renders. Timeline entries support from, to, year, title and description; the contracts support from, to and position. actions is the ordered list of per-row actions: hide, view, down, up, delete and edit. An action that is not listed is not offered, and listing both up and down is what enables drag-and-drop sorting of the list.

A section marked readonly: true offers view and nothing else, whatever its actions list says, and does not allow creating a record. Unknown values and duplicates in either list are discarded; both lists are matched without regard to case.

The typed \FGTCLB\AcademicPersons\Settings\DocumentSection carries the normalised lists and answers the capability questions - allowsAction() , getAllowedActions() , allowsCreate() and allowsDragSorting() - so the editing frontend of fgtclb/academic-persons-edit renders and offers exactly what the file declares.

The lists are enforced on both sides. The rendered buttons come from them, and so does the answer of every write endpoint of the editing frontend: an action a section does not list is refused with HTTP 403 no matter how the request was made. That includes the addresses, e-mail addresses and phone numbers of a contract, which have no section of their own and follow the actions list and the readonly flag of the contracts section.

The shipped timeline descriptions declare editor.type: ckeditor, which marks the field as rich text (the html flag) and carries a readable-text limit of 500 characters, so the editing frontend renders the rich text editor and enforces the limit with the same sanitisation contract as the rich text fields of the profile.

Impact 

The shipped sections list every row field and action that makes sense for them. A site package that overrides documentSections restates the two lists for every section it declares - a section without actions offers no action at all. Flush the TYPO3 caches after changing the file.

Feature: Configurable public profile 

Description 

The public profile detail view - the output of the Persons Detail and Persons List and Detail content elements - is assembled from the profile map of Configuration/AcademicPersons/Settings.yaml instead of a fixed template. Its structure lists name the elements of the left and the right layout column in render order, and details says per element which profile properties, which contract data or which label it shows. The shipped file renders the section navigation on the left and, on the right, the headline, the contract positions, the image, the contact data, a subline, the fold-out profile entries, the link properties and the timeline sections; below the large breakpoint the navigation moves into the content column, directly before the subline element - an override that drops subline from the right column therefore has no mobile navigation. See The profile map.

Resources/Private/Templates/Profile/Detail.html receives the map as publicProfile and dispatches every configured element to a partial of its own below Resources/Private/Partials/Profile/PublicProfile/. An empty property or relation renders nothing, and so does an element identifier the template does not know.

Everything the old, static template rendered has an element: links carries the website and publicationsLink properties with their companion title properties, and the contact rows carry the type of each email address, phone number and postal address again. What the rebuilt view has no equivalent for are the two headings of its static sections, Contracts and Additional information - every block is a configured element now and carries its own heading. The label changes that follow from this are listed in Important: Label changes of the public profile.

The view renders no <main>, no <aside> and no <h1>: it is a content element, a page may carry two of them, and the page template owns those. Its headings start at <h2> for the headline and step down from there.

The view ships its own stylesheet and a small ES module for the fold-out entries, the sticky navigation and the Bootstrap ScrollSpy, both loaded by the template through the asset collector. The six control icons of the view are registered in Configuration/Icons.php as academic-persons-envelope, academic-persons-phone, academic-persons-address, academic-persons-room, academic-persons-detail-plus and academic-persons-detail-minus, drawn in currentColor and inlined, so they take the text colour of the page. They are Bootstrap Icons and carry their MIT licence in Resources/Public/Icons/LICENSE-bootstrap-icons.txt.

The colours of the view are custom properties declared on .academic-persons-detail and are the theming hook: redeclare them on that class to change them. The stylesheet stays inside its own container, so a theme that clips its content sections with overflow: hidden cuts off the sticky navigation of the left column - see How the layout is rendered for the one rule that lifts it.

Impact 

A site package changes the layout of the public profile and the fields it shows by shipping its own profile map, without replacing the detail template. The maps are merged on the top level, so an override repeats the complete structure and details it wants - and the editable fields the editing frontend reads from the same map. Flush the TYPO3 caches after changing the file.

An installation that overrides Templates/Profile/Detail.html keeps rendering its own template; the publicProfile variable is available to it from now on. Such an override switches this feature off entirely, and it does so without any sign that it did: the page renders as it always did, the profile.structure and profile.details maps are read by the new partials only, and a change to either has no effect on that page. The timeline properties such an override prints - {item.year}, {item.yearStart}, {item.yearEnd} - are unchanged, so its entries keep rendering; what it loses is the layout, not the values.

The way back is to delete the override and configure the layout instead, or to re-derive the override from the shipped Detail.html, which is 45 lines and dispatches to the eleven partials below Resources/Private/Partials/Profile/PublicProfile/. Those eleven are the override surface now, and overriding one of them keeps the configured layout intact. Which partials the detail view stopped rendering, and which of them was deleted with it, is listed in Breaking: The partials of the detail view change.

Feature: Contracts and profile information can be hidden 

Description 

The two document record models gained access to the hidden enable field that already exists on their database tables, exactly as the contact record models did in 2.4:

  • \FGTCLB\AcademicPersons\Domain\Model\Contract
  • \FGTCLB\AcademicPersons\Domain\Model\ProfileInformation

Each of them now provides getHidden(): bool and setHidden(bool $hidden): self .

To list hidden records the two repositories provide a query that ignores the disabled enable field and nothing else:

  • ContractRepository::findByProfileIncludingHidden(Profile $profile)
  • ProfileInformationRepository::findByProfileAndTypeIncludingHidden(Profile $profile, string $type)

Both order by sorting with uid breaking ties. Every other query, and every relation of Profile , keeps respecting the enable fields, so the public views hide a hidden record as before.

The document section vocabulary of documentSections.<section>.actions gains hide. It is listed first in the shipped settings of every section, is offered only when listed, and is withdrawn by readonly: true with every other mutating action. EXT:academic_persons_edit renders it as the visibility switch of a row.

Impact 

No database change: the hidden columns and their TCA existed already. A site that overrides the actions of a section keeps its list - hide is not implied - and adds hide where the switch is wanted.

Affected Installations 

Installations that configure documentSections or read the two models in PHP.

Feature: Legacy settings overlay and migration command 

Description 

A site package that still ships the pre-3.0 shape of Configuration/AcademicPersons/Settings.yaml - the validations map with one flag list per record type, and the profileInformationsTypes map - is no longer ignored. Its two keys are mapped onto the four section maps of 3.0 at runtime, before the settings graph is built, so the installation keeps behaving as it was configured on the day of the update: the backend record editor and the editing frontend see the flags the override declared, not the shipped defaults.

The mapping is an overlay on the shipped maps. A legacy set decides the five flags the old shape knew - required , readonly , disabled , email and number - for every field of its target; a field the set does not list has none of them, exactly as it was unconfigured before, and the flags the old shape could not express - url , date , tel , textarea , html - stay as the section maps declare them. Two things are not mapped losslessly and are reported:

  • An eighth timeline entry type declared under profileInformationsTypes is not migrated. It needs a profile relation and a TCA column the settings never created; see the Breaking entry on the section based settings for how to keep one.
  • The type and fieldName of a timeline entry type are not applied. Until 2.4 the two generated the inline column of the profile table, so overriding one moved the backend relation and the frontend selection together; since 3.0 the seven relations are declared in the TCA of the profile table, and applying the override would move the frontend half alone - records created in the editing frontend would be invisible in the backend, and the other way round. The section keeps the record type and the relation field that match the TCA, and the value the override named is printed as a note. The label of the type is mapped as before.

Every package that ships a legacy key is logged once per key at warning level, naming the package, the key and the command below. The mapping is transitional and is removed in academic_persons 4.0.

The console command academic:persons:settings:migrate prints, for every active package that still ships a legacy key, the four section maps those keys are mapped onto - the document that replaces the legacy keys in that package's file - together with the notes about what could not be mapped, and exits with 1 when such a package exists, so a deployment pipeline can gate on it:

vendor/bin/typo3 academic:persons:settings:migrate > migrated.yaml
Copied!

The command never writes the file. The override lives in a site package that is under version control and usually deployed read-only, so a write would be lost on the next deployment or leave a dirty working tree; the printed maps are pasted into the package after review.

The status report of EXT:reports lists, under Academic Persons, every active package that still ships a legacy key as a warning. The status provider is registered only when EXT:reports is installed; there is no dependency on it.

Impact 

An installation with a pre-3.0 override runs on its own flags again after the update, with a warning in the log and in the status report until the override is rewritten. No cache flush is needed for the overlay to take effect: the normalised settings graph is cached under the identifier AcademicPersons_Settings_v3, while releases before 3.0 wrote AcademicPersons_Settings, so the first request after the update is a cache miss and rebuilds the graph through the overlay. A flush stays necessary after every later edit of the file, as before. The migration itself - replacing the legacy keys with the printed maps and flushing the caches - is described on the Validation settings page.

Feature: Decide what a profile image's metadata will be 

Description 

\FGTCLB\AcademicPersons\Event\ModifyProfileImageMetadataEvent announces the metadata this extension is about to write for the image of a profile, and a listener decides what is written. Whatever it leaves in getMetadata() is the field map that reaches the database; an empty map writes nothing.

The event is dispatched for each of the two records that carry image metadata, and getTargetTable() says which one is being written:

  • sys_file_metadata , the record of the file itself, written once by the frontend upload that created the file and only for the fields it found empty — title , alternative and, where typo3/cms-filemetadata adds it, copyright . This is the record an installation requiring file attributes reads, which is why the File is handed over rather than only its uid.
  • sys_file_reference , the profile's own relation row, written whenever the name of the profile record changes — from a backend save, a localization or a frontend edit.

Both records are handed over either way: getFile() is the file, whose own metadata record is $event->getFile()->getMetaData() , and getFileReference() the image relation of the profile. getRequest() is the request the write happens in, and null where there is none — on the command line, for instance.

Fields the target table does not declare are dropped before the write, so a listener may set copyright unconditionally: without typo3/cms-filemetadata the column does not exist and the value goes nowhere. System fields are dropped as well, with a warning in the log: the identity of the record, the relation it is part of, its localization, its workspace and its enable columns are the DataHandler 's, and this event writes metadata.

#[AsEventListener(identifier: 'my-extension/add-image-copyright')]
public function __invoke(ModifyProfileImageMetadataEvent $event): void
{
    if ($event->getTargetTable() !== 'sys_file_metadata') {
        return;
    }
    $metadata = $event->getMetadata();
    $metadata['copyright'] = $metadata['alternative'] ?? '';
    $event->setMetadata($metadata);
}
Copied!

Impact 

Nothing changes without a listener: the composed name of the profile record is written to the columns named above. With one, a project fills the columns its own installation adds and requires — right_of_use of fgtclb/file-required-attributes , a copyright composed differently, a caption of its own.

A listener runs inside the write of the profile record, for a backend save from within a DataHandler hook. It has to be short, and it must not write profile records itself.

Feature: Profile updates from user data announce themselves 

Description 

AbstractProfileFactory::updateProfileForUser() — the path behind the academic:updateprofiles command — persisted its changes without dispatching AfterProfileUpdateEvent ; only the profile creation did. A profile updated from its frontend user record therefore changed without its translations being synchronised and without its slug being regenerated, while the same change made through the frontend editing plugins did both.

The update path now dispatches the event per profile the update ran through, after persistAll() , carrying the persisted default language profile — the same contract as the creation path and the frontend editing flow, which both announce a profile even when every value already matched.

The skip_sync flag gates the whole update per profile now: a profile carrying it is neither data-updated nor announced. Previously the flag was only evaluated per frontend user, so a user carrying a second, synchronisable profile had the skip_sync profile updated through that side door.

Impact 

An academic:updateprofiles run now triggers the registered listeners for every synchronisable profile of the selected users. With academic_persons_edit installed that means: profile slugs are regenerated, and — with profile.allowedLanguages configured — the profile translations are created or synchronised. Installations relying on the command not touching slugs or translations should review their listener configuration before updating.

Affected Installations 

Every installation using the academic:updateprofiles command, and any installation with a frontend user connected to several profiles of which some carry skip_sync .

Important: Strict language fallback for selected profiles 

Description 

When profiles are selected explicitly (by uid) for a list plugin ( academicpersons_list / academicpersons_listanddetail with the FlexForm "selected profiles" option), a site language configured with fallbackType: strict did not hide profiles that are not translated into the requested language. Such profiles were rendered in their default language instead of being removed, unless the plugin option "fallback for non translated" was enabled on purpose.

This is not an EXT:academic_persons bug. The extension already resolves the correct language overlay type from the site configuration and only relies on Extbase persistence to apply it. The behaviour is caused by a long-standing Extbase regression that ignored the resolved language overlay type and always overlaid single records with OVERLAYS_MIXED , so untranslated records were kept.

Core references (issue and both patches):

  • Forge issue #88886"DataMapper: Consider languageOverlayMode hideNonTranslated ..."
  • Gerrit change 66694"[BUGFIX] Respect language overlay type in Extbase" (TYPO3 main line)
  • Gerrit change 94935 — the TYPO3 14.3 backport (same Change-Id)

The fix is released with TYPO3 v14.3.6 and newer (and on the TYPO3 main development line). It is not part of TYPO3 v13 and, being a behavioural change, is not backported to the v13.4 LTS.

Impact 

On TYPO3 v14.3.6 and newer the behaviour is correct out of the box: untranslated selected profiles are removed under fallbackType: strict . No configuration or code change is required in EXT:academic_persons .

On TYPO3 v13.4 (and on TYPO3 v14.3.0 - v14.3.5, before the fix shipped) the affected Extbase code still overlays with OVERLAYS_MIXED , so untranslated selected profiles keep being shown in their default language when the site language uses fallbackType: strict and the plugin fallback option is not enabled.

The two functional tests covering this behaviour ( AcademicPersonsListPluginTest and AcademicPersonsListAndDetailPluginTest , test ...WithFallbackTypeStrictWhenNotAllProfilesAreLocalized ) are therefore skipped on TYPO3 below v14.3.6 and run only where the core fix is present.

Affected Installations 

Installations that use selected profiles in a list plugin with a site language configured as fallbackType: strict and expect untranslated profiles to be hidden, running on TYPO3 v13.4 or TYPO3 v14.3.0 - v14.3.5.

Solution 

Upgrade to TYPO3 v14.3.6 or newer, which contains the core fix.

If the correct behaviour is required before that, apply the core change as a composer patch against typo3/cms-extbase until it is part of the installed core version. Cleaned patches (narrowed to typo3/cms-extbase , derived from Gerrit changes 66694 / 94935) are shipped with this extension:

TYPO3 v13.4 patch 

Documentation/Patches/extbase-88886-respect-language-overlay-type-v13.patch

extbase-88886-respect-language-overlay-type-v13.patch (TYPO3 v13.4)
diff -ruN a/Classes/Persistence/Generic/Backend.php b/Classes/Persistence/Generic/Backend.php
--- a/Classes/Persistence/Generic/Backend.php
+++ b/Classes/Persistence/Generic/Backend.php
@@ -148,11 +148,14 @@
         // This allows to fetch IDs for languages for default language AND language IDs
         // This is especially important when using the PropertyMapper of the Extbase MVC part to get
         // an object of the translated version of the incoming ID of a record.
+        // "Free" mode (OVERLAYS_OFF) is mapped to OVERLAYS_MIXED - overlays need to be enabled for the
+        // identity lookup, but hiding untranslated records is not a configured intent in free mode.
+        // This is consistent with the same handling for related objects in DataMapper->getPreparedQuery().
         $languageAspect = $query->getQuerySettings()->getLanguageAspect();
         $languageAspect = new LanguageAspect(
             $languageAspect->getId(),
             $languageAspect->getContentId(),
-            $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_ON_WITH_FLOATING : $languageAspect->getOverlayType(),
+            $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(),
             $languageAspect->getFallbackChain()
         );
         $query->getQuerySettings()->setLanguageAspect($languageAspect);
diff -ruN a/Classes/Persistence/Generic/Storage/Typo3DbBackend.php b/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
--- a/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
+++ b/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
@@ -580,9 +580,16 @@
                     $row['uid'] = $row[$GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']];
                     $row[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = 0;
                 }
-                // Currently this needs to return the default record (OVERLAYS_MIXED) if no translation is found
-                //however this is a hack and should actually use the overlay functionality as given in the original LanguageAspect.
-                $customLanguageAspect = new LanguageAspect($languageUid, $languageUid, LanguageAspect::OVERLAYS_MIXED, $languageAspect->getFallbackChain());
+                // The overlay type (and fallback chain) of the language aspect is respected, so translation
+                // behavior is consistent with the regular page / content rendering. The content language
+                // however may have been adjusted above to the language of the actually fetched record
+                // (see Note #1 and the respectSysLanguage handling), so a custom aspect is passed here.
+                $customLanguageAspect = new LanguageAspect(
+                    $languageAspect->getId(),
+                    $languageUid,
+                    $languageAspect->getOverlayType(),
+                    $languageAspect->getFallbackChain()
+                );
                 $row = $pageRepository->getLanguageOverlay($tableName, $row, $customLanguageAspect);
             }
         } elseif (is_array($row)) {
Copied!

TYPO3 v14.3 patch (v14.3.0 - v14.3.5) 

Documentation/Patches/extbase-88886-respect-language-overlay-type-v14.patch

extbase-88886-respect-language-overlay-type-v14.patch (TYPO3 v14.3.0 - v14.3.5)
--- a/Classes/Persistence/Generic/Backend.php
+++ b/Classes/Persistence/Generic/Backend.php
@@ -165,11 +165,14 @@
         // This allows to fetch IDs for languages for default language AND language IDs
         // This is especially important when using the PropertyMapper of the Extbase MVC part to get
         // an object of the translated version of the incoming ID of a record.
+        // "Free" mode (OVERLAYS_OFF) is mapped to OVERLAYS_MIXED - overlays need to be enabled for the
+        // identity lookup, but hiding untranslated records is not a configured intent in free mode.
+        // This is consistent with the same handling for related objects in DataMapper->getPreparedQuery().
         $languageAspect = $query->getQuerySettings()->getLanguageAspect();
         $languageAspect = new LanguageAspect(
             $languageAspect->getId(),
             $languageAspect->getContentId(),
-            $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_ON_WITH_FLOATING : $languageAspect->getOverlayType(),
+            $languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_OFF ? LanguageAspect::OVERLAYS_MIXED : $languageAspect->getOverlayType(),
             $languageAspect->getFallbackChain()
         );
 
--- a/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
+++ b/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
@@ -590,9 +590,16 @@
                     $row['uid'] = $row[$translationParentPointerField];
                     $row[$languageField] = 0;
                 }
-                // Currently this needs to return the default record (OVERLAYS_MIXED) if no translation is found
-                //however this is a hack and should actually use the overlay functionality as given in the original LanguageAspect.
-                $customLanguageAspect = new LanguageAspect($languageUid, $languageUid, LanguageAspect::OVERLAYS_MIXED, $languageAspect->getFallbackChain());
+                // The overlay type (and fallback chain) of the language aspect is respected, so translation
+                // behavior is consistent with the regular page / content rendering. The content language
+                // however may have been adjusted above to the language of the actually fetched record
+                // (see Note #1 and the respectSysLanguage handling), so a custom aspect is passed here.
+                $customLanguageAspect = new LanguageAspect(
+                    $languageAspect->getId(),
+                    $languageUid,
+                    $languageAspect->getOverlayType(),
+                    $languageAspect->getFallbackChain()
+                );
                 $row = $pageRepository->getLanguageOverlay($tableName, $row, $customLanguageAspect);
             }
         } elseif (is_array($row)) {
Copied!

Apply the matching patch with the composer plugin cweagans/composer-patches (see its README for installation and usage). Copy the patch into the project (for example into a patches/ directory) and reference it:

{
    "require": {
        "cweagans/composer-patches": "^1.7"
    },
    "extra": {
        "patches": {
            "typo3/cms-extbase": {
                "Respect language overlay type in Extbase (forge #88886, review 66694)": "patches/extbase-88886-respect-language-overlay-type-v13.patch"
            }
        }
    }
}
Copied!

Both patches touch only typo3/cms-extbase (Classes/Persistence/Generic/Storage/Typo3DbBackend.php and Classes/Persistence/Generic/Backend.php); EXT:academic_persons itself needs no change.

Important: Contract relation columns are NOT NULL now 

Description 

The organisational_unit, function_type and location columns of tx_academicpersons_domain_model_contract were nullable integer columns, and their TCA select fields used an empty string (respectively null) as the value of their "please select" item. PostgreSQL rejects an empty string as an integer value, so on that DBMS saving a contract with one of these selects empty failed — and so did every DataHandler localize of a contract whose source row held NULL in one of them, which the reworked translation synchronisation runs into. MySQL, MariaDB and SQLite coerce the empty string to 0 silently, which is why the defect only surfaced on PostgreSQL.

Both sides now follow the core convention for optional single-value relation selects: the empty item value is 0, the field default is 0, and the three columns are int(11) unsigned DEFAULT '0' NOT NULL.

Impact 

The database analyzer will suggest altering the three columns. On installations where existing rows hold NULL in one of them — rows created before this version and never saved since — the ALTER TABLE fails on PostgreSQL and on MySQL in strict mode until those values are updated to 0:

UPDATE tx_academicpersons_domain_model_contract
    SET organisational_unit = 0 WHERE organisational_unit IS NULL;
UPDATE tx_academicpersons_domain_model_contract
    SET function_type = 0 WHERE function_type IS NULL;
UPDATE tx_academicpersons_domain_model_contract
    SET location = 0 WHERE location IS NULL;
Copied!

An empty relation is stored as 0 from now on; Extbase resolves both NULL and 0 to an unset relation, so rendering is unaffected.

Affected Installations 

Every installation — the schema change applies everywhere. Functionally broken before this change: PostgreSQL installations only.

Important: The timeline year fields are constrained 

Description 

The three year columns of tx_academicpersons_domain_model_profile_information - year , year_start and year_end - declared

'config' => [
    'type' => 'number',
    'min' => 0,
    'max' => 9999,
    'nullable' => true,
],
Copied!

and enforced none of it. min and max are options of the TCA type input ; the type number reads its bounds from range alone. So the backend form rendered a number field without an HTML min or max attribute, and DataHandler clamped nothing on save.

The three columns now declare

'config' => [
    'type' => 'number',
    'format' => 'integer',
    'range' => [
        'lower' => 0,
        'upper' => 9999,
    ],
    'nullable' => true,
],
Copied!

which is what renders the HTML bounds and what DataHandler clamps a submitted value against; ext_tables.sql declares the three columns int(11) unsigned DEFAULT NULL, what the corrected TCA derives. The palette, the labels, the property names and the frontend rendering are unchanged.

Impact 

The backend record editor now keeps a year within 0-9999: a value above the upper bound is clamped to 9999 on save, a negative one to 0. NULL stays the empty value.

ext_tables.sql declares the three columns as int(11) unsigned DEFAULT NULL , matching what the corrected TCA derives. The database analyzer therefore offers the change of signedness. An installation that stored a negative year - which nothing in the extension ever wrote - has to correct those rows before applying it.

Affected Installations 

Every installation of fgtclb/academic-persons that edits profile information records in the TYPO3 backend.

Important: Label changes of the public profile 

Description 

The rebuilt public profile detail view (see Feature: Configurable public profile) changes three things about the labels of Resources/Private/Language/locallang.xlf. All three are relevant for an installation that ships its own translation of this file or overrides single units through $GLOBALS['TYPO3_CONF_VARS']['SYS']['locallangXMLOverride'] .

A misspelled unit id is corrected. The English file declared the unit

<trans-unit id="contracts.emailAdresses">
Copied!

with one d, while the German file always declared contracts.emailAddresses . The German target was therefore unreachable and the English source was never rendered either: the label is looked up as contracts.emailAddresses by Resources/Private/Partials/Profile/Contract/Field.html, which translates contracts.{fieldName} for the fields a list or card plugin was configured to show. The id is now spelled contracts.emailAddresses in both files. An override keyed on the old id stops taking effect and has to be re-keyed.

Three units are added for the detail view: detail.subline , detail.contact and detail.navigation - the subline of the shipped layout, the heading of the contact block and the accessible name of the section navigation.

Two units are no longer rendered by the shipped templates: detail.contracts and detail.additionalInformation . They were the headings of the two static sections the old detail template had; the rebuilt view has no equivalent, because its blocks are the configured elements and each of them carries its own heading. Both units are kept in the file for installations that reference them from their own templates.

Impact 

Re-key an XLF override of contracts.emailAdresses to contracts.emailAddresses . Nothing else has to be changed: the added units ship with an English source and a German target, and the two unused ones are still there.

Affected Installations 

Installations that translate or override EXT:academic_persons/Resources/Private/Language/locallang.xlf.

Important: Record icons follow the backend colour scheme 

Description 

The record icons of this extension were registered with the core provider \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider , which renders the default markup - the markup a typeicon_classes entry reaches - as an <img> tag. An image is opaque to CSS, so the icon kept the ink of its file whatever the backend colour scheme said, and a dark drawing stayed dark on the dark cards of the record list.

They are now registered with \FGTCLB\AcademicBase\Imaging\IconProvider\CurrentColorSvgIconProvider , which inlines the file in both markups, and the files themselves are drawn in currentColor with no colour of their own.

That covers the record icons of all nine tables this extension ships, from tx_academicpersons_domain_model_address to tx_academicpersons_domain_model_profile_information .

Impact 

The nine record icons take the text colour of the backend, so they stay legible in a dark colour scheme. Their markup is now the inlined <svg> rather than an <img> , which matters to any CSS or test that addressed the image.

The plugin icon persons_icon is a brand mark and keeps the core provider. The six control icons of the public profile were already registered with the currentColor provider and are unchanged.

Affected Installations 

Every installation of this extension.

Important: Remaining unordered queries now order by uid 

Description 

The sweep that ordered the unordered profile queries (see Important: Unordered profile queries now order by uid) missed five query paths, which kept returning rows in whatever order the database yielded:

  • ContractRepository::findAll() , which builds every contract select item in the backend (TCA itemsProcFunc and FlexForm)
  • ContractRepository::findByUids() , which resolves the contracts of the "selected contracts" plugin
  • ProfileRepository::findByUids() , which resolves the profiles of the "selected profiles" plugin
  • ProfileRepository::findByFrontendUser() , which resolves the profiles of a frontend user for the frontend editing of academic_persons_edit
  • ProfileRepository::findByDemand() with a non-empty demanded ordering — the list plugin's Sort by — which carried no tiebreaker, so profiles equal in it (two people sharing a last name) had no defined relative order

The first four now order by uid ascending; the demanded ordering keeps winning and gets uid ascending appended as a tiebreaker.

Three further methods ordered by sorting alone: AddressRepository::findByContractIncludingHidden() , EmailRepository::findByContractIncludingHidden() and PhoneNumberRepository::findByContractIncludingHidden() , which list the contact records of a contract for the frontend editing. Records an editor never reordered share a sorting value, and their relative order was whatever the database yielded. All three now append uid ascending as the tiebreaker.

Impact 

No visible change is expected: uid ascending is the order every supported database returned in practice, it is simply guaranteed now rather than coincidental.

For the two uid selection methods the order of the editor's selection is deliberately not reproduced — in() does not preserve it, and it was never delivered before. Honouring the selection order would be a behaviour change beyond making the lists reproducible.

Affected Installations 

Every installation of this extension.

Important: Route enhancers have to be limited to their pages 

Description 

The three route enhancers this extension ships below Configuration/Routes/ describe the same two views, so three of their routes are declared twice and are identical down to the mapper:

Route Declared in
/{profile_name} Detail.yaml and ListAndDetail.yaml
{localized_page}-{page} List.yaml and ListAndDetail.yaml
/{letter} List.yaml and ListAndDetail.yaml

TYPO3 offers every enhancer of a site to every page of that site unless the enhancer says otherwise, and it takes the first candidate route whose path matches and whose aspects resolve. So a site that imports more than one of the three files without saying where each applies gives all of those URLs to the file it imported first.

Only resolving is ambiguous. Generating a URL is scoped to the plugin namespace being linked, so the links keep looking right and nothing points at the configuration.

Impact 

On a site that imports ListAndDetail.yaml before Detail.yaml, the page carrying the Detail plugin answers 404 for every link the list plugins generate for it — the profile argument arrives in the namespace of the other plugin, and ProfileController::detailAction() receives nothing.

The two list routes fail more quietly, and in whichever direction the import order points: the page number or the letter arrives in the wrong namespace, and the plugin renders the unfiltered first page with status 200.

Affected Installations 

Installations whose site configuration imports more than one of List.yaml, ListAndDetail.yaml and Detail.yaml — which is what a site showing both the separate and the combined plugin needs, and what the documentation of this extension recommended without further qualification until now.

Installations that import a single one of the three files are not affected.

Solution 

Limit each enhancer to the pages that carry its plugin. With that in place the import order no longer matters:

config/sites/my_site/config.yaml
imports:
  - resource: 'EXT:academic_persons/Configuration/Routes/List.yaml'
  - resource: 'EXT:academic_persons/Configuration/Routes/ListAndDetail.yaml'
  - resource: 'EXT:academic_persons/Configuration/Routes/Detail.yaml'

routeEnhancers:
  ProfileListPlugin:
    limitToPages: [12, 13]
  ProfileListAndDetailPlugin:
    limitToPages: [14]
  ProfileDetailPlugin:
    limitToPages: [15]
Copied!

The uids are those of the pages carrying the plugin in question, in the default language: matching derives the page as l10n_parent ?: uid , so one list covers every translation of that page. Plain page uids work on every TYPO3 version this extension supports.

Important: Translation sync covers child exclude columns 

Description 

The update path of the translation synchronisation — the case where a translation already exists — re-submitted the l10n_mode=exclude values of the profile record only. A child record's exclude value changed after the child's translation existed therefore stayed stale in that translation: a contract's valid_from , an address type, a profile information year.

The datamap now covers the whole default-language inline child tree: every child's propagatable exclude values are part of the same single DataHandler pass, and the core DataMapProcessor carries them into every translation of every touched record.

Two things did not change, and are now pinned by tests:

  • File references and MM relations added to the default record after its translation exists were always carried over — the core synchronizes all exclude columns of a touched record from its database row, including the relational ones. The previously documented gap was design-inferred and did not exist. The profile image stopped being an exclude column with Breaking: The profile image translates; the same core pass carries it into every translation whose image follows the default language, so the pin of the exclude behaviour moved to a test column of the test suite and the synchronisation itself has no image-specific code.
  • enableLogging stays on: sys_log rows with userid=0 are the audit trail of what the synchronisation wrote.

Impact 

Editing an exclude column of a contract, address, email address, phone number or profile information record of an already-translated profile now reaches the record's translations on the next synchronisation, the same way it always did for the profile's own exclude columns.

Affected Installations 

Every installation using the translation synchronisation of this extension — through the frontend editing of academic_persons_edit , through the academic:updateprofiles command (which dispatches AfterProfileUpdateEvent since ACE-490), or by dispatching the event from its own hooks.

Important: Translation sync is routed through the DataHandler 

Description 

\FGTCLB\AcademicPersons\Service\RecordSynchronizer — the service behind the AfterProfileUpdateEvent listener that keeps profile translations in sync — previously wrote its translations with raw SQL. That implementation had drifted a long way from what a translation write in TYPO3 involves.

The synchronisation only ever touched the top-level row. The recursion into inline children was dead code since version 2.1.0: a contract, address, email or phone number was never translated along with its profile, and a contract added after the translation existed was never carried over. File references and MM relations (the profile image, related frontend users) were skipped even by design, and l10n_diffsource was left empty, so the backend diff view had nothing to show.

Every write was a live row, regardless of the acting workspace — an unpublished draft leaked into the live site the moment anything triggered a synchronisation from within a workspace.

The service now routes every write through the TYPO3 DataHandler . A missing translation is created with a localize command, which carries the full inline child tree, file references, MM relations and the diff source. For an existing translation, the current values of the l10n_mode=exclude columns are re-submitted as a datamap, so core's DataMapProcessor propagates them, and an inlineLocalizeSynchronize command per inline column carries children added later — including their own children.

Impact 

Child records are actually synchronised again. A profile synchronisation now translates contracts and their children, file references and MM relations, as it did before 2.1.0. Note that this also re-activates the known contacts4pages behaviour where a translated contact keeps pointing at an untranslated default-language page.

Writes are workspace aware. A backend user acting in a workspace produces versioned rows only (t3ver_wsid, t3ver_state); the live site is untouched until the workspace is published. A frontend-triggered synchronisation acting in a non-live workspace is refused entirely and logs a notice instead of writing anything.

Because the writes go through the DataHandler they now also update the reference index and record history, fire hooks, and bump tstamp on updated translations.

Affected Installations 

Every installation that enables the translation synchronisation through the profile.allowedLanguages setting of EXT:academic_persons_edit, or that dispatches AfterProfileUpdateEvent from its own hooks.

Important: Unordered profile queries now order by uid 

Description 

Five query paths of this extension executed without any ordering, so the order of their result was whatever the database happened to yield:

  • ProfileRepository::findAll()
  • ProfileRepository::findByDemand() whenever the demand yields no ordering — which is what the list plugin's Sort by option none means
  • LocationRepository::findAll() , FunctionTypeRepository::findAll() and OrganisationalUnitRepository::findAll() , which fill the location, function type and organisational unit selects of the contract form in academic_persons_edit

All five now order by uid ascending when nothing else asks for an order.

Impact 

No visible change is expected. Every supported database returned insertion order for these queries in practice, and uid ascending is that same order — the assertions of the affected functional tests are unchanged, they are simply guaranteed now rather than coincidental.

What changes is that the order is reproducible. It previously depended on the database, its version and which indexes existed, and could therefore change under an installation without any content change. That is not hypothetical: making the person tables workspace aware in the same release added an index over t3ver_oid , which gave the PostgreSQL planner a way to satisfy the t3ver_oid = 0 constraint Extbase adds for a workspace aware table, and reversed the result of exactly these queries.

The TCA default_sortby of the location, function type and organisational unit tables still does not apply to their findAll() . That was never the case and is not changed here — it orders the record lists of the backend, not the selects of the frontend edit form.

Affected Installations 

Every installation of this extension. An installation that already sets a sorting on the list plugin is unaffected either way, because an explicit ordering always won.

Important: Validation primitives moved to EXT:academic_base 

Description 

The value objects and the ViewHelper behind Configuration/AcademicPersons/Settings.yaml moved to EXT:academic_base, together with the loading, the flag normalisation and the TCA merge that AcademicPersonsSettingsFactory and AcademicPersonsSettings carried inline:

Before Now
\FGTCLB\AcademicPersons\Settings\Validation \FGTCLB\AcademicBase\Settings\Validation
\FGTCLB\AcademicPersons\Settings\ValidationSet \FGTCLB\AcademicBase\Settings\ValidationSet
\FGTCLB\AcademicPersons\ViewHelpers\ValidationEnsureViewHelper \FGTCLB\AcademicBase\ViewHelpers\ValidationEnsureViewHelper
AcademicPersonsSettings::getValidationTcaTableConfig() FGTCLB\AcademicBase\Settings\TcaValidationMerger::merge() , applied by five of the six TCA files of this extension; the profile information table merges a types fragment built from the same value objects instead

All of them are @internal . No class aliases are registered for the old names: the classes never were public API, and no extension of this set referenced them from outside EXT:academic_persons and EXT:academic_persons_edit, both of which are adapted.

Nothing changes for the YAML file itself - its location, its sets, its flags, and the package walk and top-level merge of an installation's override are all as before.

The normalised result is still cached in the core cache, but under the new identifier AcademicPersons_Settings_v3 instead of AcademicPersons_Settings. The cache entry is a PHP statement naming the classes of the settings graph, so an entry written by an earlier version references the removed \FGTCLB\AcademicPersons\Settings\ValidationSet and would fail to load before any check could reject it. The new identifier leaves such an entry untouched and unused; it disappears with the next cache flush.

Impact 

Code that type hints or instantiates the old classes fails with a class not found error and has to import the EXT:academic_base names.

A project that declares the ViewHelper namespace in a template of its own has to point it at the new location, or the template no longer renders:

xmlns:p="http://typo3.org/ns/FGTCLB/AcademicBase/ViewHelpers"
Copied!

The p:validationEnsure calls in the template stay as they are.

A project that overrides one of the frontend editing form partials of EXT:academic_persons_edit has nothing to repoint and nothing to keep: the Extbase form flow those partials belonged to is replaced in 3.0, the Partials/Profile/Forms/ directory is gone with it, and no template of either extension declares the namespace any more. Delete the override and start from what the new editing view renders - the changelog entry Breaking: Replaced the profile editing view of EXT:academic_persons_edit lists the removed templates and the partials that took their place.

No cache flush is required for the settings themselves - see the cache identifier above - but flushing all caches after the update is harmless and removes the orphaned entry.

3.x Changes by type 

This lists all changes to the Academic Persons extension of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

ChangeLog v2 

Every change to the Academic Persons extension is documented here.

Also available 

2.4 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: Profile synchronization now includes hidden profiles and users 

Description 

The profile synchronization (profile update command) previously skipped frontend users that were disabled or whose profile was hidden. Such records were neither selected by \FGTCLB\AcademicPersons\Provider\FrontendUserProvider::getUsersWithProfileResult() (the automatic hidden restriction excluded them) nor resolved by \FGTCLB\AcademicPersons\Domain\Repository\ProfileRepository::findByFrontendUser() .

The synchronization now processes hidden profiles and disabled frontend users as well and keeps their data up to date, without changing their visibility.

The same applies to the profile creation (profile create command): \FGTCLB\AcademicPersons\Provider\FrontendUserProvider::getUsersWithoutProfileResult() now also returns disabled frontend users and \FGTCLB\AcademicPersons\Profile\AbstractProfileFactory::createProfileForUser() resolves the frontend user ignoring its visibility, so a profile is created for a disabled frontend user as well.

Impact 

A hidden profile or a profile of a disabled frontend user that was relying on being skipped by the synchronization is now updated again on the next synchronization run. Its data (name, contact records, ...) is overwritten with the current frontend user data, while the hidden state itself is kept untouched.

A disabled frontend user without a profile now also receives a newly created profile on the next profile create run.

Only the deleted state is still respected; both the frontend user visibility (fe_users.disable) and the profile hidden field are ignored for the profile synchronization and creation.

Affected Installations 

All installations using the profile synchronization (EXT:academic_persons create/update profile commands) together with manually hidden profiles or disabled frontend users.

Migration 

If a profile should be excluded from the synchronization, use the dedicated skip_sync flag of the profile instead of hiding the profile or disabling the frontend user. Hiding a profile now only controls its frontend visibility, not whether it is synchronized.

Breaking: Renamed itemsProcFunc handler classes to FormEngine namespace 

Description 

While introducing the shared PSR-14 event \FGTCLB\AcademicBase\Event\ModifyTcaSelectFieldItemsEvent to allow modifying the select items of fields populated by an itemsProcFunc, the two itemsProcFunc handler classes shipped by EXT:academic_persons received a proper namespace and class names suitable for classes providing itemsProcFunc implementations.

The following classes have been renamed and moved:

  • \FGTCLB\AcademicPersons\Backend\Form\ContractItemsProcFunc is now \FGTCLB\AcademicPersons\Backend\FormEngine\ContractItems .
  • \FGTCLB\AcademicPersons\Backend\Form\ProfileShowFieldsItemProcFunc is now \FGTCLB\AcademicPersons\Backend\FormEngine\ProfileShowFieldsItems .

Additionally the entry method of the profile show fields handler has been streamlined from showFields() to itemsProcFunc() to match the other handlers.

The references to these classes in the shipped plugin FlexForms have been adjusted accordingly.

Impact 

Referencing the old class names or the old showFields() method throws a PHP error. This affects TCA/FlexForm itemsProcFunc configurations, projects extending or replacing these classes and any code instantiating them directly.

Affected Installations 

Installations that reference the old classes in own TCA/FlexForm configuration, extend or replace them, or call them directly.

Migration 

Replace all usages of the old class names with the new ones and use the itemsProcFunc() method for both handlers:

FGTCLB\AcademicPersons\Backend\Form\ContractItemsProcFunc->itemsProcFunc
=> FGTCLB\AcademicPersons\Backend\FormEngine\ContractItems->itemsProcFunc

FGTCLB\AcademicPersons\Backend\Form\ProfileShowFieldsItemProcFunc->showFields
=> FGTCLB\AcademicPersons\Backend\FormEngine\ProfileShowFieldsItems->itemsProcFunc
Copied!

Projects that only replaced the shipped items should consider using the new \FGTCLB\AcademicBase\Event\ModifyTcaSelectFieldItemsEvent event listener instead of a custom itemsProcFunc.

Breaking: Site sets and static templates have been restructured 

Description 

The TypoScript and the page TSconfig of this extension were shipped twice: the static template read Configuration/TypoScript/Default/, and the two site sets fgtclb/academic-persons-default and fgtclb/academic-persons-standalone shipped their own constants.typoscript and setup.typoscript, each of them a single @import of that folder. The page TSconfig existed as one flat file, Configuration/TSconfig/page.tsconfig, and was not selectable on a page at all.

Both mechanisms now read one physical copy of every file, and both of them deliver the extension per content element instead of as one block:

  • Configuration/TypoScript/Default/ still holds the shared plugin.tx_academicpersons block — all six content elements are driven by one Extbase plugin, so there is one copy of it and every component folder names it in its own include_static_file.txt.
  • Configuration/TypoScript/<Component>/ is what the static template registers and what the component set points its typoscript key at.
  • Configuration/TSconfig/<Component>/page.tsconfig holds the page TSconfig of one content element and is what the page field Page TSconfig offers and what the set points its pagets key at.
  • Configuration/TypoScript/Full/ and Configuration/TSconfig/Full/page.tsconfig are the aggregates for installations that do not use site sets.

The content elements are now hidden by default. The always-included Configuration/page.tsconfig removes all six from the selectable content element types, and the page TSconfig of a component adds its own back — so an element is offered where it is wanted instead of on every page of every installation. The TCA registration itself did not move, so the frontend renders existing records exactly as before. Editing such a record in the backend is a different matter — read the warning below before upgrading.

Two defaults changed as well. plugin.tx_academicpersons.demand.groupBy and plugin.tx_academicpersons.demand.sortBy were empty in constants.typoscript while the site sets declared lastNameAlpha and title for the very same paths, so the two mechanisms configured a site differently. Both now carry the same value everywhere.

demand.sortBy is additionally corrected to lastName. The value the site sets shipped, title, is not one the extension accepts: the extension configuration demand.allowedSortByValues allows firstName and lastName only, and ProfileRepository::getOrderingsFromDemand() drops anything else. A site on a site set therefore asked for an ordering it never got.

Impact 

The sys_template records of this extension keep working. Both values that installations store today — EXT:academic_persons/Configuration/TypoScript/Default and EXT:academic_persons/Configuration/TypoScript/Standalone — still resolve, and both still deliver the plugin configuration. Only the labels shown next to them in the record changed.

A site that has no site set and did not set plugin.tx_academicpersons.demand.groupBy itself now groups a profile list by the first letter of the last name, because that is the default the site sets already applied. Set the constant to an empty value to keep an ungrouped list.

A site on a site set that did not set plugin.tx_academicpersons.demand.sortBy itself now sorts a profile list by last name. It asked for title before and got no ordering at all, because the value was rejected. Set the setting to firstName for the other accepted ordering; there is no supported way back to "no ordering" through this setting.

A site package that imported one of the removed files by path fails to resolve it. @import of a missing file is silent, so this shows up as missing configuration rather than as an error message.

None of the six content elements is offered in the backend until the page TSconfig of its component is included, through the site set or through the page field Page TSconfig. This affects every installation: before this change the extension hid nothing.

The sets fgtclb/academic-persons-default and fgtclb/academic-persons-standalone keep their names and keep delivering everything, so a site configuration that depends on either needs no change.

Affected Installations 

Every installation that uses one of the content elements of this extension, plus installations that import one of the shipped files from an own site package.

Migration 

Add the page TSconfig entry, which did not exist before, in the page record of the site root, tab Resources, field Page TSconfig: Academic Persons: All components (academic_persons), stored as EXT:academic_persons/Configuration/TSconfig/Full/page.tsconfig. Without it the content elements are not selectable any more, and existing records of them lose their CType when they are saved from the backend form.

Sites that use a site set instead need no migration — but they must not use both mechanisms at once, see the Configuration chapter.

The static template entries in the sys_template record need no migration either. Their labels changed, their stored values did not:

Stored value Old label New label
EXT:academic_persons/Configuration/TypoScript/Default Academic Persons Settings (academic_persons) Academic Persons: Shared plugin settings (academic_persons)
EXT:academic_persons/Configuration/TypoScript/Standalone Academic Persons Standalone (academic_persons) Academic Persons: Standalone page (academic_persons)

Adjust every @import in an own site package:

Old path New path
EXT:academic_persons/Configuration/TypoScript/constants.typoscript EXT:academic_persons/Configuration/TypoScript/Default/constants.typoscript
EXT:academic_persons/Configuration/TypoScript/setup.typoscript EXT:academic_persons/Configuration/TypoScript/Default/setup.typoscript
EXT:academic_persons/Configuration/TypoScript/Standalone/constants.typoscript EXT:academic_persons/Configuration/TypoScript/Default/constants.typoscript
EXT:academic_persons/Configuration/TypoScript/Standalone/setup.typoscript EXT:academic_persons/Configuration/TypoScript/Default/setup.typoscript and, for the page object, EXT:academic_persons/Configuration/TypoScript/StandalonePage/setup.typoscript
EXT:academic_persons/Configuration/TSconfig/page.tsconfig EXT:academic_persons/Configuration/TSconfig/Full/page.tsconfig

The first two were shipped as @deprecated one line forwards to Configuration/TypoScript/Default/ and are removed with this change.

A site configuration may name the new component sets instead of the aggregate:

Set Delivers
fgtclb/academic-persons New name of the aggregate, delivers through the component sets below.
fgtclb/academic-persons-default Unchanged in name, now an alias of fgtclb/academic-persons.
fgtclb/academic-persons-standalone Unchanged in name, now the aggregate plus the page object.
fgtclb/academic-persons-list The Persons List content element only.
fgtclb/academic-persons-list-and-detail The Persons List and Detail content element only.
fgtclb/academic-persons-detail The Persons Detail content element only.
fgtclb/academic-persons-card The Contacts content element only.
fgtclb/academic-persons-selected-profiles The Profiles: Selected Profiles content element only.
fgtclb/academic-persons-selected-contracts The Profiles: Selected Contracts content element only.

Feature: Configurable frontend user phone number types 

Description 

Telephone and fax numbers imported from frontend users no longer carry the hard-coded values phone and fax, which are not selectable in the shipped phone-number type list. Two new extension configuration options say which type each source field imports as:

profile.feuser.telephoneNumberType = business
profile.feuser.faxNumberType = business
Copied!

Both default to business, which is part of the shipped types.phoneNumberTypes. Each value is validated against that list before it is written: a value the installation does not offer falls back to the undefined type '', which the backend select ships as its first item. The synchronisation therefore never writes a type the backend cannot resolve.

An existing selectable type is an editor decision and is left alone. Only the two historical values are corrected, and only where they are not selectable on that installation: an installation that genuinely offers phone or fax as types keeps them.

Impact 

New and updated imports carry a configured, selectable type. Installations that want the imported numbers typed differently — or not typed at all — set the two options; no other configuration changes.

Feature: Dispatch ModifyTcaSelectFieldItemsEvent in itemsProcFunc handlers 

Description 

The following itemsProcFunc handlers provided by EXT:academic_persons now dispatch the shared PSR-14 event \FGTCLB\AcademicBase\Event\ModifyTcaSelectFieldItemsEvent :

  • \FGTCLB\AcademicPersons\Backend\FormEngine\ContractItems
  • \FGTCLB\AcademicPersons\Backend\FormEngine\ProfileShowFieldsItems

Previously the shipped select items could only be adjusted by adding items directly (without influence on the ordering) or by implementing a custom itemsProcFunc replacing the shipped one. Both ways were neither convenient nor testable.

With the dispatched event, projects can modify the available select items for the backend (FormEngine) - and, where the handler is reused, for the frontend - through a single PSR-14 event listener, instead of dealing with a dozen dedicatedly named events for the same purpose.

Example 

EXT:my_ext/Classes/EventListener/ModifyTcaSelectFieldItemsEventListener.php
<?php

declare(strict_types=1);

namespace MyVendor\MyExt\EventListener;

use FGTCLB\AcademicBase\Event\ModifyTcaSelectFieldItemsEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'my-ext/modify-academic-persons-tca-select-items')]
final class ModifyTcaSelectFieldItemsEventListener
{
    public function __invoke(ModifyTcaSelectFieldItemsEvent $event): void
    {
        $tableName = $event->getParameters()['table'];
        $fieldName = $event->getParameters()['field'];
        if ($tableName !== 'tt_content') {
            // Not the table we want to handle. Skip.
            return;
        }
        if ($fieldName === 'settings.showFields') {
            $this->modifyPersonsShowFieldsSelectItems($event);
        }
        if ($fieldName === 'settings.selectedContracts') {
            $this->modifyPersonsContractSelectItems($event);
        }
    }

    private function modifyPersonsShowFieldsSelectItems(
        ModifyTcaSelectFieldItemsEvent $event,
    ): void {
        $parameters = $event->getParameters();
        $parameters['items'][] = [
            'label' => BackendUtility::getItemLabel('tx_academicpersons_domain_model_contract', 'position'),
            'value' => 'contracts.position',
            'group' => 'contracts',
        ];
        $event->setParameters($parameters);
    }

    private function modifyPersonsContractSelectItems(
        ModifyTcaSelectFieldItemsEvent $event,
    ): void {
        $parameters = $event->getParameters();
        $parameters['items'][] = [
            'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang_be.xlf:custom_contract',
            'value' => 10,
        ];
        $event->setParameters($parameters);
    }
}
Copied!

Feature: "Show hidden records" plugin option for the person plugins 

Description 

A new boolean plugin option Show hidden records ( settings.showHiddenRecords , checkbox/toggle, default off) was added to the following plugins:

  • List ( academicpersons_list )
  • List and detail ( academicpersons_listanddetail )
  • Card ( academicpersons_card )
  • Selected profiles ( academicpersons_selectedprofiles )
  • Selected contracts ( academicpersons_selectedcontracts )
  • Detail ( academicpersons_detail )

The List, List and detail and Card plugins share the core-version-aware List.xml flexform; Selected profiles, Selected contracts and Detail use their own SelectedProfiles.xml, SelectedContracts.xml and Detail.xml flexforms. All are provided for both the TYPO3 v12 and v13 flexform data structures.

When the option is enabled, the affected frontend listing includes hidden (disabled) records, independent of the Context API visibility settings. Only the hidden enable column (disabled) is ignored; the deleted, starttime/endtime and fe_group restrictions stay in effect.

The single-profile Detail plugin resolves its profile through Extbase argument mapping, which normally respects enable fields. When the option is enabled, the detail view re-resolves the referenced profile including hidden records, so a hidden profile can be displayed on its detail page.

Impact 

Editors can now opt in per plugin instance to display hidden profiles or contracts in the frontend, for example to preview intentionally hidden records without changing the global preview settings. The option is off by default, so existing plugin instances keep their current behaviour.

Affected Installations 

All installations using the EXT:academic_persons extension starting with version 2.4. No action is required for existing installations.

Feature: Synchronize hidden profiles 

Description 

The profile synchronization (profile update command, \FGTCLB\AcademicPersons\Service\ProfileUpdateCommandService ) now also keeps hidden profiles and profiles of disabled frontend users up to date. Previously a frontend user that was disabled or whose profile was hidden was excluded from the synchronization completely, so the profile was never updated and no synchronization events were dispatched for it.

The \FGTCLB\AcademicPersons\Domain\Repository\ProfileRepository method findByFrontendUser() gained an optional argument to include hidden profiles:

  • findByFrontendUser(int $frontendUserUid, bool $showHidden = false): QueryResultInterface

The synchronization calls it with $showHidden = true , while the frontend display keeps the default ( $showHidden = false ) and therefore continues to respect the visibility.

The visibility itself is never changed by the synchronization — that stays the responsibility of the \FGTCLB\AcademicPersons\Profile\ProfileFactoryInterface implementation — so a manually hidden profile stays hidden while its data is kept in sync.

The profile create command keeps skipping frontend users that already have a profile, including a hidden one, so no duplicate profiles are created for them. It now also creates a profile for a disabled frontend user that does not have one yet: \FGTCLB\AcademicPersons\Provider\FrontendUserProvider::getUsersWithoutProfileResult() returns disabled frontend users and \FGTCLB\AcademicPersons\Profile\AbstractProfileFactory::createProfileForUser() resolves the frontend user ignoring its visibility.

Impact 

Hidden profiles and profiles of disabled frontend users are no longer silently excluded from the synchronization. To exclude a profile from synchronization, use the dedicated skip_sync flag of the profile instead of hiding it or disabling the frontend user.

Affected Installations 

All installations using the profile synchronization (EXT:academic_persons create/update profile commands) starting with version 2.4.

Feature: Visibility (hidden) support for contact record models 

Description 

The contact record models gained access to the hidden enable field that already exists on their database tables:

  • \FGTCLB\AcademicPersons\Domain\Model\Address
  • \FGTCLB\AcademicPersons\Domain\Model\Email
  • \FGTCLB\AcademicPersons\Domain\Model\PhoneNumber

Each of them now provides:

  • getHidden(): bool
  • setHidden(bool $hidden): self

To work with hidden records the matching repositories ( AddressRepository , EmailRepository , PhoneNumberRepository ) provide two new methods that ignore the disabled enable field:

  • findByContractIncludingHidden(int $contractUid): QueryResultInterface
  • findByUidIncludingHidden(int $uid): ?<Model>

These are used by EXT:academic_persons_edit (optional) to let profile owners show and hide their contact records in the frontend, while the public profile display keeps excluding hidden records via the default Extbase enable field handling.

Impact 

The profile synchronization keeps matching and updating existing contact records even when they are hidden, and no longer resets their visibility. Integrators can use the new model accessors and repository methods to handle contact record visibility programmatically.

Affected Installations 

All installations using the EXT:academic_persons extension starting with version 2.4.

Migration 

No migration is required, except you extended and replaced the extbase model in projects - then you need to adopt the newly added properties and setter/getter methods with the same signatures.

Important: Extended ProfileDemand, findByUids() and demand handling 

Description 

To support the new "Show hidden records" plugin option, the person demand and repository layer gained the following additions. All of them are non-breaking (new optional parameter / new transport flag with defaults):

  • \FGTCLB\AcademicPersons\Domain\Model\Dto\ProfileDemand has a new showHiddenRecords transport flag with getShowHiddenRecords(): bool and setShowHiddenRecords(bool): ProfileDemand accessors (default false ). It mirrors the existing transport-only properties ( storagePages , fallbackForNonTranslated ) and is likewise announced as a future addition to \FGTCLB\AcademicPersons\Domain\Model\Dto\DemandInterface (the interface carries the commented signatures and the repository guards the call with method_exists() ).
  • \FGTCLB\AcademicPersons\Domain\Repository\ProfileRepository::findByDemand() honours ProfileDemand::getShowHiddenRecords() via applyDemandSettings() .
  • \FGTCLB\AcademicPersons\Domain\Repository\ProfileRepository::findByUids() and \FGTCLB\AcademicPersons\Domain\Repository\ContractRepository::findByUids() gained an appended optional bool $showHidden = false parameter.
  • \FGTCLB\AcademicPersons\Domain\Repository\ProfileRepository::findByUidIncludingHidden(int $uid): ?Profile is a new method that resolves a single profile by uid including hidden (disabled) records. It is used by the detail view.
  • \FGTCLB\AcademicPersons\Controller\ProfileController reads $this->settings['showHiddenRecords'] in adoptSettings() , cardAction() , selectedProfilesAction() and selectedContractsAction() and threads it into the repositories. The new initializeDetailAction() re-resolves the profile argument via findByUidIncludingHidden() when the option is enabled, because the default Extbase argument mapping respects enable fields.

When the flag/parameter is set, the query ignores only the disabled (hidden) enable field via the Extbase query settings; the deleted, starttime/endtime and fe_group restrictions stay in effect.

Impact 

The change is non-breaking: the new demand flag defaults to false , the new findByUids() parameter has a default value, and no existing method signature changed in an incompatible way. Projects that build a ProfileDemand or call findByUids() themselves can opt in via setShowHiddenRecords(true) respectively the new parameter.

Affected Installations 

Only installations that extend or replace the ProfileDemand DTO, the ProfileRepository , the ContractRepository or the ProfileController need to take the additions into account. All other installations are unaffected.

Important: Imported telephone records use a stable identifier 

Description 

The import identifier of a synchronised phone-number record used to be built from its type, so it read phone:fe_users:<uid> for a telephone number. That made the identity of the record depend on a value that is now configurable, and a reconfiguration would have created a second record instead of updating the first one.

The identifier is now built from the source field of fe_users and never from the type:

  • telephone numbers: telephone:fe_users:<uid> — changed
  • fax numbers: fax:fe_users:<uid> — unchanged

The record is matched on that identifier alone. A record still carrying the legacy phone:fe_users:<uid> is recognised, renamed and — where its type is still the unselectable phone — retyped, in one step. Where a contract carries both a legacy and a canonical record, the canonical one wins and the legacy one is left untouched, because its provenance cannot be established safely.

Impact 

Two stored values change for records the synchronisation writes: import_identifier for telephone records, and type wherever it still held phone or fax and that value is not selectable on the installation. Anything outside this extension that keys on phone:fe_users:<uid> has to be adjusted.

No upgrade wizard is shipped, deliberately. Existing records are repaired by the next academic:updateprofiles run — the same command that wrote them — and that run is what the affected installations already schedule. A bulk migration would have to decide what the synchronisation deliberately refuses to decide: which of two colliding records is the real one, whether soft-deleted and workspace rows take part, and what to write where the configured type is not selectable.

Six cases are consequently not repaired by a synchronisation run, and an installation that needs them corrected has to act deliberately:

  • profiles carrying skip_sync = 1
  • frontend users that are soft-deleted
  • profiles whose tx_academicpersons_feuser_mm relation was removed
  • records on page ids excluded by the configured --include-pids or --exclude-pids
  • installations that ran an import once and never run it again
  • records whose fe_users.telephone has since been emptied, which the synchronisation removes rather than repairs

Affected Installations 

Every installation that imports profiles from frontend users with academic:createprofiles or academic:updateprofiles .

Important: New required dependency fgtclb/environment-state-manager 

Description 

EXT:academic_persons now depends on the standalone extension fgtclb/environment-state-manager and declares it consistently in both composer.json ( "fgtclb/environment-state-manager": "^1.0" ) and ext_emconf.php ( 'environment_state_manager' => '1.0.0-1.99.99' ).

The extension was switched from the internal, now deprecated \FGTCLB\AcademicBase\Environment subsystem to the extracted fgtclb/environment-state-manager extension (namespace \FGTCLB\EnvironmentStateManager ), which it uses in its profile command services. The dependency is therefore required at runtime.

Impact 

Composer-managed installations pull fgtclb/environment-state-manager in automatically when EXT:academic_persons is updated to 2.4; no action is required.

Classic, non-composer installations (TER / extension manager) must install the environment_state_manager extension in addition to EXT:academic_persons , otherwise the extension cannot be activated.

Affected Installations 

Only non-composer installations updating to EXT:academic_persons 2.4 need to install the additional extension manually. Composer-managed installations are unaffected.

Feature: Add demand DTO to ModifyListProfilesEvent 

Description 

PSR-14 Event \FGTCLB\AcademicPersons\Event\ModifyListProfilesEvent got following new methods:

  • getProfileDemand(): ProfileDemand
  • setProfileDemand(ProfileDemand $profileDemand): void

This gives event listeners the ability to redo a query with the same or further enriched demand object to replace the retrieved profiles result already existing in the event and/or enrich data in the view.

Feature: Add academic:updateprofiles command 

Description 

EXT:academic_persons provided the academic:createprofiles for quite a long time now to create profile automatically for frontend users taking data from TYPO3 fe_users records. That can be extended by extension or projects to get import data from other sources like LDAP or other external identity providers.

To further improve that handling the academic:updateprofiles is now added in a similar way to handle updates of profile data based on the source using the scheduler, for example to update data imported from external LDAP.

import_identifier varchar(170) DEFAULT '' NOT NULL is added to all extension tables and all domain models got extended to have that property along with setter und getter in place. Existing imported profile data needs to be updated in the project to have the identifier in place and custom create/update profile implementation can use that field to flag it with their data.

Further a skip_sync field is added to tx_academicpersons_domain_model_profile defaulting to false ((INT)0). If this field is set to true/1 the update command excludes these records in a early stage and do not call or dispatch any further methods or events.

Important note 

Custom profile factory implementations needs to be updated due to the extended \FGTCLB\AcademicPersons\Profile\ProfileFactoryInterface interface and implement the update handling part.

This is breaking and needs to be addressed on a update.

2.1 Changes 

Table of contents

Breaking Changes 

Features 

Deprecation 

Important 

Breaking: ProfilesController::selectedProfilesAction() no longer dispatches ModifyListProfilesEvent 

Description 

ProfilesController::selectedProfilesAction() dispatched the ModifyListProfilesEvent PSR14 event accidentally due to copy&paste when introducing the new plugin and action for 2.0.x. This event is no longer dispatched for this action, instead the new and correct event ModifySelectedProfilesEvent is now dispatched.

Affected Installations 

EXT:academic_partners installations listening to the ModifyListProfilesEvent event.

Migration 

Adapt any listeners/subscribers accordingly to the correct event.

Breaking: Removed partials 

Description 

Some partials got removed as the templating structure has changed.

Impact 

Those partials include:

  • Resources/Private/Partials/List/AlphabetPagination.html
  • Resources/Private/Partials/List/ListItem.html
  • Resources/Private/Partials/List/Pagination.html
  • Resources/Private/Partials/SelectedContracts/ListItem.html

Affected Installations 

EXT:academic_partners installations overriding those partials.

Migration 

Adapt overrides accordingly to the new templating structure.

Breaking: Replace constructor DI with inject-methods in AbstractProfileFactory 

Description 

Using constructor dependency injection in abstract classes defines the constructor as API, which should be avoided by using the inject-method approach and allows to implement classes using constructor DI without the requirement to deal and align with parent (abstract) class constructor and passing it down.

AbstractProfileFactory used constructor DI and therefore violated the above described design pattern.

Constructor DI arguments are now replaced with inject-methods in the abstract FGTCLBAcademicPersonsProfileAbstractProfileFactory.

See Autowiring other Methods (e.g. Setters and Public Typed Properties)

Impact 

AbstractProfileFactory used constructor DI and therefore violated the above described design pattern.

Constructor DI arguments are now replaced with inject-methods in the abstract FGTCLBAcademicPersonsProfileAbstractProfileFactory.

Affected Installations 

Installations using the abstract and defining own constructor DI arguments.

Migration 

Implementation using the abstract and defining own constructor DI arguments needs to remove the removed parent arguments and avoid calling the parent constructor.

Additionally, the SymfonyContractsServiceAttributeRequired attribute is used for the inject methods to tell symfony DI that these inject methods needs to be called and are mandatory - beside having a visually glue for developers.

Feature: Add academic:createprofiles options --include-pids and --exclude-pids 

Description 

EXT:academic_persons provides a command to create profiles for frontend users, extendable by dispatching events to allow devs customizing the profile creation in projects, for example to base it on special needs like retrieving updated user data from LDAP oder other services.

Until now, all frontend users without profiles on any pid has been fetched, which does not respect use-cases where frontend users for dedicated logins are required and profile creation is not wanted, needed or suitable.

This change adds following new options to the provided command vendor/bin/typo3 academic:createprofiles:

  • --include-pids: comma-separated list of storage pid's from which frontend users should be fetched (only).
  • --exclude-pids: comma-separated list of storage pid's from which frontend users should be ignored (skipped).

Important note 

While both options can be used together it is important to know that --exclude-pids takes higher priorities and are ignored even if pid is also included in include-pids.

Feature: Allow modifying default and settings pageTitleFormat for detail view 

Description 

It's possible to set a pageTitleFormat in the plugin settings for plugins using the ProfileController::detailAction(), which is used in ProfileTitleProvider to set the HTML page title for the person profile pages.

This change extends the existing ModifyDetailProfileEvent, which is dispatched in the ProfileController::detailAction(), to make the default and the setting pageTitleFormat changeable using an PSR-14 event listener.

This gives developers the ability to implement a wide range of use-cases in projects, for example adding a prefix to the format based on the site configuration or similar.

New ModifyDetailProfileEvent methods:

  • getDefaultPageTitleFormat(): string
  • setDefaultPageTitleFormat(string $defaultPageTitleFormat): void
  • getSettingsPageTitleFormat(): string
  • setSettingsPageTitleFormat(string $settingsPageTitleFormat): void
  • getPageTitleFormatToUse(): string

The getPageTitleFormatToUse(): string is a calculated function to get the aggregated format to use, which allows checking the result in event listeners and determine the format finally used as detail view page title.

The original default pageTitleFormat is ProfileTitleProvider::DETAIL_PAGE_TITLE_FORMAT.

Feature: Dispatch PSR-14 event ModifySelectedContractsEvent in ProfilesController::selectedContractsAction() 

Description 

ProfileController::selectedContractsAction() dispatches now the new PSR-14 ModifySelectedContractsEvent.

The event provides following methods:

  • getContracts(): QueryResultInterface return current result set.
  • setContracts(QueryResultInterface $contracts): void to allow setting a custom resultset.
  • getView(): FluidViewInterface|CoreViewInterface return the current view to allow assigning custom values to the view.
  • getPluginControllerActionContext(): PluginControllerActionContextInterface to provide more context information

Feature: Dispatch PSR-14 event ModifySelectedProfilesEvent in ProfilesController::selectedProfilesAction() 

Description 

ProfileController::selectedProfilesAction() dispatches now the new PSR-14 ModifySelectedProfilesEvent instead of erroneous copied listAction event ModifyListProfilesEvent, which is no longer dispatched. That should not be that of an issue for most implementations.

The event provides following methods:

  • getProfiles(): QueryResultInterface return current result set.
  • setProfiles(QueryResultInterface $profiles): void to allow setting a custom resultset.
  • getView(): FluidViewInterface|CoreViewInterface return the current view to allow assigning custom values to the view.
  • getPluginControllerActionContext(): PluginControllerActionContextInterface to provide more context information

Feature: Introduce localized pageTitleFormat placeholder (LLL:EXT:) 

Description 

It's a valid use-case to use a pageTitleFormat for the person profile detail page view as HTML page title including localized text as placeholders and could be implemented using the PSR-14 event ModifyProfileTitlePlaceholderReplacementEvent dispatched in the ProfileTitleProvider.

Localization is a generic feature and it's most likely that it's use-full for a broader audience this change adds now support for localization placeholder in the format:

%%LLL:EXT:<extension-key>/Resources/.../locallang.xlf:identifier%%

Note that no context fallback detection is made like within fluid templates or extbase context areas and a valid relative path for the default language file within a extension needs to be provided.

Functional tests are added to cover the new feature basically and provide some examples, using a dedicated test fixture extension.

Feature: Introduce PluginControllerActionContext suitable 

Description 

A new readonly DTO object PluginControllerActionContext is introduced and is attached to dispatched PSR-14 events in ProfileController actions.

Impact 

Following main getters are provided:

  • getApplicationType(): ApplicationType to return the TYPO3 application type

for the current request. `getExtbaseRequestParameters(): ?ExtbaseRequestParameters` to retrieve extbase attribute from request as a simple accessor. getRequest(): ServerRequestInterface to return the current request. `getSettings(): array` to retrieve raw plugin settings (TypoScript, FlexForm). getSite(): ?Site to retrieve resolved site configuration. * getLanguage(): ?SiteLanguage to retrieve resolves site language.

Following getters dispatches to ExtbaseRequestParameters methods and returning null in case the request attribute is not set in the request:

  • getActionName(): ?string
  • getControllerName(): ?string
  • getControllerObjectName(): ?string
  • getControllerExtensionKey(): ?string
  • getControllerExtensionName(): ?string
  • getPluginName(): ?string

Feature: Introduce PSR-14 ModifyProfileTitlePlaceholderReplacementEvent in ProfileTitleProvider 

Description 

With recent changes a series of features has been implemented to make the HTML title tag for person profile pages more flexible, with a placeholder based FlexForm options and also allowing to influence the default and the setting pageTitleFormat.

The used ProfileTitleProvider already looks into the format string and provides the ability to replace placeholders, which matches getters in the profile.

To provide even more flexibility, this change introduces a new PSR-14 Event ModifyProfileTitlePlaceholderReplacementEvent, which is dispatched for each placeholder enriched with quite a handfull of use-full context information.

Following methods are available on the event:

  • getPluginControllerActionContext(): PluginControllerActionContextInterface containing the request along with easy access methods to site, siteLanguage and extbase plugin information and the plugin settings.
  • getProfile(): Profile the current person profile to display.
  • getPlaceholder() the original/raw placeholder identifier.
  • getReplacement() the value to replace the placeholder with, which may differ already if a earlier event listener changed the value using setReplacement().
  • setReplacement(string $replacement): void to set the value used to replace the placeholder.

This event allows project to implement custom placeholders and the replacement without using old-school xclassing technique.

Feature: pageTitleFormat FlexForm option for person detail view 

Description 

It's now possible to define the format used to generate the HTML PageTitle for the detail view of persons in the frontend, using the TYPO3 PageTitle API.

The default format used based on Profile extbase model data is:

%%TITLE%% %%FIRST_NAME%% %%MIDDLE_NAME%% %%LAST_NAME%%

To allow easier customization in project, a new FlexForm option pageTitleFormat has been added to listanddetail plugin and as single new option for the detail plugin, which uses TCA type=input combined with a ValuePicker to allow picking from a list of pre-defined formats while still making it possible to define own custom format directly on plugin usage.

The mapping from placeholder to extbase model is based on transforming the placeholder to camelcase using first character after separators and prefix it with get, and if the getter exists it is called to retrieve the value.

For example:

which calls Profile->getFirstName() to retrieve the replacement value from the detail view profile.

The whole process contains some behaviour, which needs to be kept in mind:

  • Leading and trailing spaces are trimmed from each value(placeholder).
  • Multiple spaces are removed from the whole format string.
  • Leading and trailing spaces are trimmed from the whole format pattern, after placeholder resolving has been processed.

Example for allowed characters as placeholder identifier:

Note that most of them has no handling for matching person profile getters, but are use-full for advanced replacement using the experimental PSR-14 event.

Important: Basic bootstrap styling 

Description 

The default templating now supports basic bootstrap styling and is semantically optimized to also not lack any major accessibility.

Breaking: Migrated extbase plugins from list_type to CType 

Description 

TYPO3 v13 deprecated the tt_content sub-type feature, only used for CType=list sub-typing also known as list_type and mostly used based on old times for extbase based plugins. It has been possible since the very beginning to register Extbase Plugins directly as CType instead of CType=list sub-type, which has now done.

Technically this is a breaking change, and instances upgrading from 1.x version of the plugin needs to update corresponding tt_content records in the database and eventually adopt addition, adjustments or overrides requiring to use the correct CType.

Impact 

The change relates to following plugins:

  • academicpersons_detail
  • academicpersons_list
  • academicpersons_listanddetail
  • academicpersons_selectedcontracts
  • academicpersons_selectedprofiles

Affected Installations 

All installations using the above listed plugins prior V2.0.

Migration 

A TYPO3 UpgradeWizard academicPersons_pluginUpgradeWizard is provided to migrate plugins from CType=list to dedicated CTypes matching the new registration.

2.x Changes by type 

This lists all changes to the Academic Persons extension of minor versions grouped by their type.

Table of contents

Breaking Changes 

Features 

Deprecations 

Important notes 

Sitemap