Extending Classes in TYPO3

As a TYPO3 extension developer you most certainly will experience situations where you want to extend existing classes, either from the TYPO3 core or from another extension. Fortunately, TYPO3 provides several ways to achieve just that:

Signals, Slots and Hooks

The preferred way of extending existing functionality should always be to use existing Signals and Hooks. Both provide a clearly defined extension interface to developers, which is why it’s pretty safe to use them.

Hooks

There are two flavors of Hooks: function-based hooks, using GeneralUtility::callUserFunction(), and object-based hooks, using GeneralUtility::getUserObject(). Each flavor has to be handled a bit differently, but everything can be achieved with both methods.

In practice, most hooks are function-based because they are a bit more flexible and easier to handle for the developer: You are flexible in naming the hook functions and splitting them up into several classes.

Function-based hooks look something like this:

if (is_array($TYPO3_CONF_VARS['SC_OPTIONS']['my/path/class.original.php']['hookName'])) {  
    $_params = array(
        'hookParam1' => &$hookParam1,
        'hookParam2' => &$hookParam2
    );
    foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['my/path/class.original.php']['hookName'] as $_funcRef) {
        \TYPO3\CMS\Core\Utility\GeneralUtility::callUserFunction(
            $_funcRef,
            $_params,
            $this
        );
    }
}

In this case, the hook function gains access to the data provided in $_params as well as a reference to the original object ($this). And because $_params will be provided to the hook function by reference, the hook can even modify the input data (if the result in $_params is handled correctly by the original extension).

If you want to bind your code to that Hook, you can add the following line to your extension’s ext_localconf.php:

// typo3conf/ext/my_extension/ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['my/path/class.original.php']['hookName'][$_EXTKEY] = 'VENDOR\MyExtension\Hook\OriginalHook->hookName';

and define the specified method:

// typo3conf/ext/my_extension/Classes/Hook/OriginalHook.php
namespace VENDOR\MyExtension\Hook;

class OriginalHook {  
    public function hookName(&$params, $ref) {
        // Your code
    }
}

Object-based hooks work like this:

if (is_array($TYPO3_CONF_VARS['SC_OPTIONS']['my/path/class.original.php']['hookName'])) {  
    foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['my/path/class.original.php']['hookName'] as $_classRef) {
        $_procObj = \TYPO3\CMS\Core\Utility\GeneralUtility::getUserObj($_classRef);

        if (method_exists($_procObj, 'hookName')) {
            $_procObj->hookName(
                $hookParam1,
                $hookParam2,
                $this
            );
        }
    }
}

Again, to use that hook, you have to add a line to your ext_localconf.php file:

// typo3conf/ext/my_extension/ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['my/path/class.original.php']['hookName'][$_EXTKEY] = 'VENDOR\MyExtension\Hook\OriginalHook';

Now you are only providing a reference to a class, which means that the hook calls in the original class enforce the internal structure of the hook class (e. g. you have to name the method hookName()):

// typo3conf/ext/my_extension/Classes/Hook/OriginalHook.php
namespace VENDOR\MyExtension\Hook;

class OriginalHook {  
    public function hookName(&$hookParam1, $hookParam2, $ref) {
        // Your code
    }
}

As a reminder, this is the call in the original class:

$_procObj->hookName(
    $hookParam1,
    $hookParam2,
    $this
);

This also means that if you define parameters as references, as I did with $hookParam1, you can modify them.

You can read more about this topic in the TYPO3 API Overview chapter about Hooks.

Signals and Slots

While Hooks are still widely used both in the TYPO3 core and in various community extensions, Signals and Slots are clearly the modern approach and should be preferred whenever possible. However, documentation about Signals/Slots is mostly absent, except for the FLOW3 documentation (from which the concept was backported) as well as a few blog posts.

The basic concept of Signals and Slots is similar to Hooks: The original object emits events – Signals – and 3rd party code can be bound to those events using Slots.

A Signal in the original class looks like this:

$this->signalSlotDispatcher->dispatch(
    __CLASS__,
    'signalName',
    array(&$slotParam1, $slotParam2, $this)
);

The Signal Slot Dispatcher will most likely be injected using TYPO3’s dependency injection:

/**
 * @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
 * @inject
 */
protected $signalSlotDispatcher;  

If you want to bind your code to that Signal, you simply add the following lines to your ext_localconf.php:

// typo3conf/ext/my_extension/ext_localconf.php

$signalSlotDispatcher = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
    'TYPO3\\CMS\\Extbase\\SignalSlot\\Dispatcher'
);
$signalSlotDispatcher->connect(
    'VENDOR\\OriginalExtension\\Foo\\Original',
    'signalName',
    'VENDOR\\MyExtension\\Slot\\OriginalSlot',
    'handleSignalName'
);

and create the specified class and method:

// typo3conf/ext/my_extension/Classes/Slot/OriginalSlot.php
namespace VENDOR\MyExtension\Slot;

class OriginalSlot {  
    public function handleSignalName(&$slotParam1, $slotParam2, \VENDOR\OriginalExtension\Foo\Original $ref) {
        // Your code
    }
}

As you can see, I’ve defined the first parameter $slotParam1 as a reference to be able to modify its value. Note that this only works if the parameter has been defined as reference in the Signal call as well (see above) – unless of course the parameter contains an object, in which case you can omit the &.

During the creation of this post, I stumbled upon another thoroughly written blog post about Hooks, Signals and Slots which is clearly worth a read: Use TYPO3: Signals and Hooks in TYPO3.

Overwriting Classes with XCLASS

But what if the class in question doesn’t provide Signals or Hooks? TYPO3 covers that case as well by providing XCLASS, a way to overwrite entire classes. As with Hooks and Signals/Slots there is an old and a new way to xclass classes. Fortunately, the old, ugly way is mostly dead.

The new way, however, also has its own quirks. To begin, there are several locations where classes can be overwritten:

  • in ext_localconf.php using PHP code
  • in ext_typoscript_setup.txt using TypoScript
  • in your static TypoScript template (or any other TypoScript template)

In addition, classes can be overwritten both globally and locally (only for the current extension). However, this distinction is only possible using TypoScript, the PHP method will always be global.

All of this only works if the class in question is loaded via the ObjectManager, GeneralUtility::makeInstance() or TYPO3’s dependency injection. It’s also important to get the naming of the class right: \VENDOR\MyClass is not the same as VENDOR\MyClass. In general, the leading slash should be omitted for $objectManager->get() calls, so it should be omitted in overwrite definitions as well.

Let’s see how each method works when overwriting OriginalUtility with ReplacementUtility:

Overwrite classes using PHP:

// typo3conf/ext/my_extension/ext_localconf.php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects']['VENDOR\\OriginalExtension\\Utility\\OriginalUtility'] = array(
    'className' => 'VENDOR\\MyExtension\\Utility\\ReplacementUtility'
);

Overwrite classes globally using TypoScript:

config.tx_extbase.objects {  
    VENDOR\OriginalExtension\Utility\OriginalUtility.className = VENDOR\MyExtension\Utility\ReplacementUtility
}

Overwrite classes locally for one extension using TypoScript:

plugin.tx_myextension.objects {  
    VENDOR\OriginalExtension\Utility\OriginalUtility.className = VENDOR\MyExtension\Utility\ReplacementUtility
}

Loading order

With all those options to overwrite classes, the loading order becomes more and more important because each class can only be overwritten once. This is especially true if there is another extension that already overwrites the class you want to overwrite.

There are a few rules that determine which overwrite wins:

  • local overwrites take precedence over global overwrites
  • included (!) static TypoScript takes precendence over ext_typoscript_setup.txt
  • all TypoScript takes precedence over PHP
    • in early stages, PHP may take precedence if TypoScript hasn’t been loaded yet
  • both ext_localconf.php and ext_typoscript_setup.txt will be loaded in extension loading order (whatever that is …)
  • classes from non-ExtBase extensions can only be overwritten using the PHP method

So, if you want to overwrite a core class, it might be necessary to use the PHP method simply because TypoScript might not be available yet. However, if another extension uses ext_typoscript_setup.txt to overwrite a class you want to overwrite as well, you might want to use static TypoScript to overwrite the already existing overwrite.

You can find more information about this topic in the TYPO3 API Overview chapter about XCLASS.

Extending Database Models

If you want to extend existing database models, you most likely would use XCLASS to overwrite the original model class with your model class, which then extends the original class. However, if your goal is to add another record type of the entity in question, you can accomplish that using class hierarchies:

Fortunately, the official documentation about this topic is quite good, which is why I’ll just refer to it at this point: Extbase & Fluid: Modeling the Class Hierarchy