How to use existing hooks in your own extension

One way for extensions to take over control of certain parts of TYPO3's core is using hooks. This article gives an example of how to register your own userfunction at an existing hook and shows the best practice for using hooks in general.
Intended Audience This tutorial is intended for all kinds of TYPO3 extension programmers who want to override or take influence on certain behaviour of TYPO3's core or some extension, by taking advantage of the concept of hooks. A basic knowledge of extension programming and some general idea about using objects in PHP is assumed. You should read the <link http: typo3.org documentation document-library>section about hooks in the document TYPO3 Core APIs, available at <link http: www.typo3.org>typo3.org What the hook ...? A hook is some easy way to extend or replace a certain part of the source by your own methods. They are often used for providing pre- and post-processing userfunctions in the core as well as giving control totally to a method provided by some extension author. The big advantage of using a hook instead of the older X-Class mechanism is the ability to enable more than one extension to extend the same class or method. This has been discussed in the <link http: typo3.org documentation mailing-lists dev-list-archive>developer mailinglist for some time and is also explained in the TYPO3 Core APIs document. The drawback is, that a hook has to exist before you can use it. So if you are in a situation in which you would like to extend a core or extension's functionality, just ask the source's author to provide a hook for you in the next release.

A real world example

In case you still don't know what hooks are about, maybe a real world example will give you a clue. Let's assume you want to create an extension which provides a basic workflow. Authors and chief editors Your website's authors are allowed to create pages, but those will be hidden by default. The hidden field in the pages table is excluded by using the exclude-field mechanism and thus not available to these authors. The chief-editor has more rights, as he is in charge of checking the content, his authors have produced. Whenever he accepts an article, he just un-hides the page and thus makes it visible on your website. This mini-workflow works perfectly until an author, who still has the right to edit his page, modifies it. Instantly the new content will be visible to the public bypassing the control of the chief-editor! Our goal is now to make sure, the page will be hidden automatically, whenever an author, not being member of the chief-editors group, modifies the page or one of it's content elements. Locating the hook To achieve that goal, we first have to find out what happens when you update a page or a content element in the backend. Because you know the core quite well and have read Inside TYPO3 and TYPO3 Core APIs, you know that submitted data from the backend forms is being processed in the class t3lib_tcemain. I won't discuss the details of TCEmain in this article, but I guess you'll understand our hook we're going to use without deeper knowledge of TYPO3's core classes. The place to begin our search for a possible hook therefore is a method called process_datamap which resides in t3lib/class.t3lib_tcemain.php. I've been here Surprisingly someone else already had a need for some hook in this method: browsing through the lines, we find a neat pre-processing hook implemented by some core developer:    // Hook: processDatamap_postProcessFieldArray

reset($hookObjectsArr);

while (list(,$hookObj) = each($hookObjectsArr)) {

    if (method_exists ($hookObj, 'processDatamap_postProcessFieldArray')) {

        $hookObj->processDatamap_postProcessFieldArray ($status, $table, $id, $fieldArray, $this);

    } 

}

This was great luck of course, because usually you don't find a pre-made hook which perfectly suits your needs. But if there is a need for a new entry point and that hook makes sense to implement, just contact the author of that paticular part of code and he'll gladly provide you with a hook.

Insert coin and choose a flavour

As you already know, hooks come in two flavours: callUserFunction and the getUserObj. Most of the core developers prefer the tasty getUserObj way - and not only because it has some fancy design pattern behind it (to be honest, it's not that fancy at all, but at least it has something to do with objects).

Anyways, before you can register your method at some hook, you'll have to find out what kind of species you deal with. In our little example, we came across some getUserObj style.

Going the getUserObj way

Instead of explaining the different implementations, I'll just go ahead with describing how we implement our userfunction the getUserObj way. Later on I will show the same implementation using the callUserFunction method, so you'll easily see the differences.

Create and include your own class

The first thing we need, is a new class which contains our user function. This is one important fact about getUserObj driven hooks: You create a class (usually one for each function you want to override in the original source) which contains as many methods as hooks you want to implement.

In our example we want to use one hook in the function process_datamap in the class tce_main. That's why I create a new file, calledclass.tx_myextension_tcemainprocdm.php containing a class named tx_myextension_tcemainprocdm.

Of course it would have been nicer to use the whole function name (..._tcemain_processdatamap) as a filename, but according to the <link http: typo3.org documentation document-library>coding guidelines we are only allowed to use 31 characters for our filename.

Finally we create an empty function in our new classed, with exactly the same name mentioned in the hook. Our class should now look like this: 

class tx_myextension_tcemainprocdm {

    function processDatamap_postProcessFieldArray ($status, $table, $id, &$fieldArray, &$this) {

        // here comes the code

    }

Register the method

We have seen in the code snippet from TCEmain, that an array called $hookObjectsArr is being traversed and a method processDatamap_postProcessFieldArray is being called if it exists. Aparrently we have to make sure that our new class is also available as an instantiated object in just that array $hookObjectsArr.

The solution lies at the very beginning of the process_datamap function:

 // First prepare user defined objects (if any) for hooks which extend this function:

$hookObjectsArr = array();

if (is_array ($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'])) {

    foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'] as $classRef) {

        $hookObjectsArr[] = &t3lib_div::getUserObj ($classRef);

    } 

As you can see, we just have to put our class name into the global variable called $TYPO3_CONF_VARS (this is also the recommended place for managing hooks in general). We will do that by adding one line to the ext_localconf.php of our extension:

$GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = 'tx_myextension_tcemainprocdm';

Note that the ...php']['processDatamapClass'] variable is an array and we just add another value (our class name).

There is another thing we have to take care of: Make sure that the file containing our new class is loaded when it's going to be used in the hook. You could either add a require_once statement into the ext_tables.php of your extension - or, which is the reccomended way, use the following hint:

Hint: Include and register your class simultaneously

There is a nice feature in the getUserObj method which allows us to combine the two steps loading the file and registering the class: Instead of putting require_once into ext_tables.php and registering your class in ext_localconf.php, you may register your new class with a line like this (in ext_localconf.php):

$GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = 'EXT:myextension/class.myextension_tcemainprocdm.php:tx_myextension_tcemainprocdm';

Another great advantage of this method is that your file will only be included if it's really used! 

Hide page - Part I

Now we finally may create the actual implementation providing the new functionality.  Let's have a look at our new function again:

function processDatamap_postProcessFieldArray ($status, $table, $id, &$fieldArray, &$reference) {

As you can see, the variable &$fieldArray was passed by reference, which means that we can modify it if we want! And we do want to change it: If there is a backend user logged in, not being member of a certain backend usergroup (namely the chief editor's group), we want to set the hidden field to 1.

if (!t3lib_div::inList ($reference->BE_USER->user['usergroup'], $chiefEditorsUsergroup)) {

    if ($status == 'update' && $table == 'pages') {

        $fieldArray['hidden'] = 1;

    } 

Through the $reference parameter, we have complete access to the parent object providing the hook. In our case we use TCEmain's variable BE_USER which contains an instance of the current backend user object.

Of course you will have to set $chiefEditorsUsergroup to some meaningful value ...

Hide page - Part II

That works just fine, however it doesn't really make sense yet. The page will only be hidden if someone not being the chief editor edits the page, that is: some record in the table pages. But what if someone modifies a content element being part of that page, how can we intercept that? Just like that:

class.tx_myextension_tcemain.php: 

   1:function processDatamap_postProcessFieldArray ($status,$table,$id,&$fieldArray,&$reference) {

   2:     if (!t3lib_div::inList ($reference->BE_USER->user['usergroup'], $chiefEditorsUsergroup)) {

   3:         if ($status == 'update' && $table == 'pages') {

   4:             $fieldArray['hidden'] = 1;

        }

   7:         if ($status == 'update' && $table == 'tt_content') {

   8:             $row = t3lib_BEfunc::getRecord ($table, $id);

  10:             if (is_array ($row)) {

  11:                 $dataArr = array ();

  12:                 $dataArr['pages'][$row['pid']]['hidden'] = 1;

  14:                 $tce = t3lib_div::makeInstance('t3lib_TCEmain');

  15:                 $tce->start($dataArr, array());

  16:                 $tce->process_datamap();

            }

        }

    }

}

Noticed that we call process_datamap from the userfunction itsel?. We have to create a new instance of TCEmain (line 14) to achieve that.

Doing it with callUserFunc

Now, what if this hook was provided using t3lib_div::callUserFunc? The code in TCEmain would look similar to this: 

if (is_array ($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapPostProcFieldArray'])) {

    $_params = array (

        'status' => $status,

        'table' => $table,

        'id' => $id,

        'fieldArray' => &$fieldArray,

    ); 

    foreach ($TYPO3_CONF_VARS['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapPostProcFieldArray'] as $funcRef) {

        $content .= t3lib_div::callUserFunction($funcRef, $params, $this);

    } 

Instead of passing the variables direktly to a method with a pre-defined name, they are stuffed into an array ($params) and passed to the user function through t3lib_div::callUserfunction. This doesn't look as nice as the other approach, see that?

To make things a bit shorter, I just summarize the changes to the different files below, I guess you get the point: 

ext_localconf.php: 

$GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapPostProcFieldArray'][] = 'tx_myextension_tcemainprocdm->processDatamap_postProcFieldArray';

ext_tables.php: 

require_once(t3lib_extMgm::extPath('myextension').'class.tx_myextension_tcemainprocdm.php');

class.tx_myextension_tcemainprocdm.php: 

function processDatamap_postProcFieldArray (&$params, &$reference) {

    if (!t3lib_div::inList ($reference->BE_USER->user['usergroup'], $requiredUsergroup)) {

        if ($params['status'] == 'update' && $params['table'] == 'pages') {

            $params['fieldArray']['hidden'] = 1;

        } 

        if ($params['status'] == 'update' && $params['table'] == 'tt_content') {

            $row = t3lib_BEfunc::getRecord ($params['table'], $params['id']);

            if (is_array ($row)) {

                $dataArr = array ();

                $dataArr['pages'][$row['pid']]['hidden'] = 1;

                $tce = t3lib_div::makeInstance('t3lib_TCEmain');

                $tce->start($dataArr, array());

                $tce->process_datamap();

            } 

        } 

    } 

}

Conclusion

Using hooks is a quite simple but also powerful approach of extending the core or even other extensions. At least it helps you to get rid of the XCLASS dilemma in many situations Let's summarize the steps which are necessary to use a getUserObj hook:
  1. Locate a possible hook in the script you want to extend. If there is none, suggest a new hook to the source's author

  2. Create a new class for each function you want to extend

  3. Create a method for each hook you want to use. It is named after the method called in the hook itself. Use the same interface for your function which is defined in the hook.

  4. Register your class by adding your extension name, filename and classname to the appropriate place in the global $TYPO3_CONF_VARS

  5. Study the source you want to extend and know what you are about to do!

The main differences when using the a callUserFunction hook instead:
  1. make sure that your class containing the user function gets included (by require_once)

  2. all parameters are passed in a single array, the second parameter contains a reference to the parent class.

  3. You may collect all userfunctions in one big class, it's up to you.

Notes about this paticular hook At the time of this writing, the hook discussed in this article is only available in the latest CVS version of TYPO3's core (namely the HEAD branch for the upcoming version 3.7.0). If you would like to try the examples yourself, you'll have to checkout the CVS version or just wait for 3.7.0 being released ...

About the author

Robert Lemke is an active member of the TYPO3 developer community and founding member of the TYPO3 Association. He lives in Lüneburg / Germany together with Heike, his lovely girl-friend and Vibiemme, their espresso-machine.