Versioning and Workspaces

TYPO3 provides a feature called "workspaces", whereby changes can be made to the content of the web site without affecting the currently visible (live) version. Changes can be previewed and go through an approval process before publishing.

The technical background and a practical user guide to this feature are provided in the "workspaces" system extension manual.

All the information necessary for making any database table compatible with workspaces is described in the TCA reference (in the description of the "ctrl" section and in the description of the "versioningWS" property).

You might want to turn the workspace off for certain tables. The only way to do so is with a Configuration/TCA/Overrides/example_table.php:

EXT:some_extension/Configuration/TCA/Overrides/example_table.php
$GLOBALS['TCA']['example_table']['ctrl']['versioningWS'] = false;
Copied!

See TYPO3 Sitepackage Tutorial and Storing in the overrides folder .

The concept of workspaces needs attention from extension programmers. The implementation of workspaces is however made, so that no critical problems can appear with old extensions;

  • First of all the "Live workspace" is no different from how TYPO3 has been working for years so that will be supported out of the box (except placeholder records must be filtered out in the frontend with t3ver_state != , see below).
  • Secondly, all permission related issues are implemented in DataHandler so the worst your users can experience is an error message.

However, you probably want to update your extension so that in the backend the current workspace is reflected in the records shown and the preview of content in the frontend works as well. Therefore this chapter has been written with instructions and insight into the issues you are facing.

Frontend challenges in general

For the frontend the challenges are mostly related to creating correct previews of content in workspaces. For most extensions this will work transparently as long as they use the API functions in TYPO3 to request records from the system.

The most basic form of a preview is when a live record is selected and you lookup a future version of that record belonging to the current workspace of the logged in backend user. This is very easy as long as a record is selected based on its "uid" or "pid" fields which are not subject to versioning: call sys_page->versionOL() after record selection.

However, when other fields are involved in the where clause it gets dirty. This happens all the time! For instance, all records displayed in the frontend must be selected with respect to "enableFields" configuration! What if the future version is hidden and the live version is not? Since the live version is selected first (not hidden) and then overlaid with the content of the future version (hidden) the effect of the hidden field we wanted to preview is lost unless we also check the overlaid record for its hidden field (->versionOL() actually does this). But what about the opposite; if the live record was hidden and the future version not? Since the live version is never selected the future version will never have a chance to display itself! So we must first select the live records with no regard to the hidden state, then overlay the future version and eventually check if it is hidden and if so exclude it. The same problem applies to all other "enableFields", future versions with "delete" flags and current versions which are invisible placeholders for future records. Anyway, all that is handled by the \TYPO3\CMS\Core\Domain\Repository\PageRepository class which includes functions for "enableFields" and "deleted" so it will work out of the box for you. But as soon as you do selection based on other fields like email, username, alias etc. it will fail.

Summary

Challenge: How to preview elements which are disabled by "enableFields" in the live version but not necessarily in the offline version. Also, how to filter out new live records with t3ver_state set to 1 (placeholder for new elements) but only when not previewed.

Solution: Disable check for enableFields/where_del_hidden on live records and check for them in versionOL on input record.

Frontend implementation guidelines

Any place where enableFields() are not used for selecting in the frontend you must at least check that t3ver_state != 1 so placeholders for new records are not displayed.

If you need to detect preview mode for versioning and workspaces you can use the Context object. GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('workspace', 'id', 0); gives you the id of the workspace of the current backend user. Used for preview of workspaces.

Use the following API function for support of version previews in the frontend:

$GLOBALS['TSFE']->sys_page->versionOL($table, &$row, $unsetMovePointers=FALSE)

Versioning Preview Overlay.

Generally ALWAYS used when records are selected based on uid or pid. If records are selected on other fields than uid or pid (e.g. "email = ....") then usage might produce undesired results and that should be evaluated on individual basis.

Principle: Record online! => Find offline?

Example:

This is how simple it is to use this record in your frontend plugins when you do queries directly (not using API functions already using them):

EXT:some_extension/Classes/SomeClass.php
$result = $queryBuilder->executeQuery();
while ($row = $result->fetchAssociative()) {
    $GLOBALS['TSFE']->sys_page->versionOL($table,$row);
    if (is_array($row)) {
        // ...
    }
    // ...
}
Copied!

When the live record is selected, call ->versionOL() and make sure to check if the input row (passed by reference) is still an array.

The third argument, $unsetMovePointers = FALSE, can be set to TRUE when selecting records for display ordered by their position in the page tree. Difficult to explain easily, so only use this option if you don't get a correct preview of records that has been moved in a workspace (only for "element" type versioning)

Frontend scenarios impossible to preview

These issues are not planned to be supported for preview:

  • Lookups and searching for records based on other fields than uid, pid or "enableFields" will never reflect workspace content since overlays happen to online records after they are selected.

    • This problem can largely be avoided for versions of new records because versions of a "New"-placeholder can mirror certain fields down onto the placeholder record. For the tt_content table this is configured as:

      shadowColumnsForNewPlaceholders'=> 'sys_language_uid,l18n_parent,colPos,header'

      so that these fields used for column position, language and header title are also updated in the placeholder thus creating a correct preview in the frontend.

    • For versions of existing records the problem is in reality reduced a lot because normally you don't change the column or language fields after the record is first created anyway! But in theory the preview can fail.
    • When changing the type of a page (e.g. from "Standard" to "External URL") the preview might fail in cases where a look up is done on the doktype field of the live record.

      • Page shortcuts might not work properly in preview.
      • Mount Points might not work properly in preview.
  • It is impossible to preview the value of count(*) selections since we would have to traverse all records and pass them through ->versionOL() before we would have a reliable result!
  • In \TYPO3\CMS\Core\Domain\Repository\PageRepository::getPageShortcut(), PageRepository->getMenu() is called with an additional WHERE clause which will ignore changes made in workspaces. This could also be the case in other places where PageRepository->getMenu() is used (but a search shows it is not a big problem). In this case we will for now accept that a wrong shortcut destination can be experienced during previews.

Backend challenges

The main challenge in the backend is to reflect how the system will look when the workspace gets published. To create a transparent experience for backend users we have to overlay almost every selected record with any possible new version it might have. Also when we are tracking records back to the page tree root point we will have to correct pid-values. All issues related to selecting on fields other than pid and uid also relates to the backend as they did for the frontend.

Backend module access

Changed in version 12.0

If you are using an older version of TYPO3, please use the version switcher on the top left of this document to go to the respective version.

You can restrict access to backend modules by setting the value of the workspaces key in the backend module configuration:

EXT:my_extension/Configuration/Backend/Modules.php
return [
    'web_examples' => [
        'parent' => 'web',
        // Only available in live workspace
        'workspaces' => 'live',
        // ... other configuration
    ],
];
Copied!

The value can be one of:

  • * (always)
  • live
  • offline

Detecting current workspace

You can always check what the current workspace of the backend user is by reading WorkspaceAspect->getWorkspaceId(). If the workspace is a custom workspace you will find its record loaded in $GLOBALS['BE_USER']->workspaceRec.

The values for workspaces is either 0 (online/live) or the uid of the corresponding entry in the sys_workspace table.

Using DataHandler with workspaces

Since admin users are also restricted by the workspace it is not possible to save any live records when in a workspace. However for very special occasions you might need to bypass this and to do so, you can set the instance variable \TYPO3\CMS\Core\DataHandling\DataHandler::bypassWorkspaceRestrictions to TRUE. An example of this is when users are updating their user profile using the "User Tool > User Settings" module; that actually allows them to save to a live record (their user record) while in a draft workspace.

Moving in workspaces

TYPO3 v4.2 and beyond supports moving for "Element" type versions in workspaces. A new version of the source record is made and has t3ver_state = 4 (move-to pointer). This version is necessary in order for the versioning system to have something to publish for the move operation.

When the version of the source is published a look up will be made to see if a placeholder exists for a move operation and if so the record will take over the pid / "sortby" value upon publishing.

Preview of move operations is almost fully functional through the \TYPO3\CMS\Core\Domain\Repository\PageRepository::versionOL() and \TYPO3\CMS\Backend\Utility\BackendUtility::workspaceOL() functions. When the online placeholder is selected it looks up the source record, overlays any version on top and displays it. When the source record is selected it should be discarded in case shown in context where ordering or position matters (like in menus or column based page content). This is done in the appropriate places.

Persistence in-depth scenarios

The following section represents how database records are actually persisted in a database table for different scenarios and previously performed actions.

Placeholders

Workspace placeholders are stored in field t3ver_state which can have the following values:

-1
  • new placeholder version
  • the workspace pendant for a new placeholder (see value 1)
0
  • default state
  • representing a workspace modification of an existing record (when t3ver_wsid > 0)
1
  • new placeholder
  • live pendant for a record that is new, used as insertion point concerning sorting
2
  • delete placeholder
  • representing a record that is deleted in workspace
4
  • move pointer
  • workspace pendant of a record that shall be moved

Overview

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
10 0 0 128 0 0 0 0 0 example.org website
20 10 0 128 0 0 0 0 0 Current issues
21 10 0 256 0 0 0 20 1 Actualité
22 10 0 384 0 0 0 20 2 Neuigkeiten
30 10 0 512 0 0 0 0 0 Other topics
... ... ... ... ... ... ... ... ... ...
41 30 0 128 1 0 1 0 0 Topic #1 new
42 -1 0 128 1 41 -1 0 0 Topic #2 new
uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
12 20 0 256 0 0 0 0 0 Article #2
13 20 0 384 0 0 0 0 0 Article #3
... ... ... ... ... ... ... ... ... ...
21 -1 0 128 1 11 0 0 0 Article #1 modified
22 -1 0 256 1 12 2 0 0 Article #2 deleted
23 -1 0 384 1 13 4 0 0 Article #3 moved
25 20 0 512 1 0 1 0 0 Article #4 new
26 -1 0 512 1 25 -1 0 0 Article #4 new
27 20 1 640 0 0 1 0 0 Article #5 discarded
28 -1 1 640 0 27 -1 0 0 Article #5 discarded
29 41 0 128 1 0 1 0 0 Topic #1 Article new
30 -1 0 128 1 29 -1 0 0 Topic #1 Article new
... ... ... ... ... ... ... ... ... ...
31 20 0 192 1 0 1 11 1 Entrefilet #1 (fr)
32 -1 0 192 1 31 -1 11 1 Entrefilet #1 (fr)
33 20 0 224 1 0 1 11 2 Beitrag #1 (de)
34 -1 0 224 1 33 -1 11 2 Beitrag #1 (de)

Scenario: Create new page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
10 0 0 128 0 0 0 0 0 example.org website
... ... ... ... ... ... ... ... ... ...
30 10 0 512 0 0 0 0 0 Other topics
... ... ... ... ... ... ... ... ... ...
41 30 0 128 1 0 1 0 0 Topic #1 new
42 -1 0 128 1 41 -1 0 0 Topic #2 new
  • record uid = 41 defines sorting insertion point page pid = 30 in live workspace, t3ver_state = 1
  • record uid = 42 contains actual version information, pointing back to new placeholder, t3ver_oid = 41, indicating new version state t3ver_state = -1

Scenario: Modify record

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
... ... ... ... ... ... ... ... ... ...
21 -1 0 128 1 11 0 0 0 Article #1 modified
  • record uid = 21 contains actual version information, pointing back to live pendant, t3ver_oid = 11, using default version state t3ver_state = 0

Scenario: Delete record

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
12 20 0 256 0 0 0 0 0 Article #2
... ... ... ... ... ... ... ... ... ...
22 -1 0 256 1 12 2 0 0 Article #2 deleted
  • record uid = 22 represents delete placeholder t3ver_state = 2, pointing back to live pendant, t3ver_oid = 12

Scenario: Create new record on existing page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 20 0 512 1 0 1 0 0 Article #4 new
26 -1 0 512 1 25 -1 0 0 Article #4 new
  • record uid = 25 defines sorting insertion point on page pid = 20 in live workspace, t3ver_state = 1
  • record uid = 26 contains actual version information, pointing back to new placeholder, t3ver_oid = 25, indicating new version state t3ver_state = -1

Scenario: Create new record on page that is new in workspace

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
29 41 0 128 1 0 1 0 0 Topic #1 Article new
30 -1 0 128 1 29 -1 0 0 Topic #1 Article new
  • record uid = 29 defines sorting insertion point on page pid = 41 in live workspace, t3ver_state = 1
  • record uid = 30 contains actual version information, pointing back to new placeholder, t3ver_oid = 29, indicating new version state t3ver_state = -1
  • side-note: pid = 41 points to new placeholder of a page that has been created in workspace

Scenario: Discard record workspace modifications

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
27 20 1 640 0 0 1 0 0 Article #5 discarded
28 -1 1 640 0 27 -1 0 0 Article #5 discarded
  • previously records uid = 27 and uid = 28 have been created in workspace (similar to Scenario: Create new record on existing page)
  • both records represent the discarded state by having assigned deleted = 1 and t3ver_wsid = 0

Scenario: Create new record localization

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
11 20 0 128 0 0 0 0 0 Article #1
... ... ... ... ... ... ... ... ... ...
31 20 0 192 1 1 0 11 1 Entrefilet #1 (fr)
32 -1 0 192 1 31 -1 11 1 Entrefilet #1 (fr)
33 20 0 224 1 0 1 11 2 Beitrag #1 (de)
34 -1 0 224 1 33 -1 11 2 Beitrag #1 (de)
  • principles of creating new records with according placeholders applies in this scenario
  • records uid = 31 and uid = 32 represent localization to French sys_language_uid = 1, pointing back to their localization origin l10n_parent = 11
  • records uid = 33 and uid = 34 represent localization to German sys_language_uid = 2, pointing back to their localization origin l10n_parent = 11

Scenario: Create new record, then move to different page

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 30 0 512 1 0 1 0 0 Article #4 new & moved
26 -1 0 512 1 25 -1 0 0 Article #4 new & moved
  • previously records uid = 25 and uid = 26 have been created in workspace (exactly like in Scenario: Create new record on existing page), then record uid = 25 has been moved to target target page pid = 30
  • record uid = 25 directly uses target page pid = 30

Scenario: Create new record, then delete

uid pid deleted sorting t3ver_wsid t3ver_oid t3ver_state l10n_parent sys_language_uid title
... ... ... ... ... ... ... ... ... ...
25 20 1 512 0 0 1 0 0 Article #4 new & deleted
26 -1 1 512 0 25 -1 0 0 Article #4 new & deleted