June 10, 2009

Bidirection MM-relations

Category: Core

By: Francois Suter

This article tries to shed some light on a little-documented feature of the TCA.

Ever since TYPO3 4.2, it is possible to do bidirectional MM-relations between two tables (before that you needed extension "mmforeign"). The main point of a bidirectional MM-relation is that you can define the relation from both ends, as you would expect from a normal relational model. In TYPO3 you can additionally have a different sorting on both ends.

Unfortunately it is not quite easy to take advantage of this feature, as the documentation (Core APIs) is somewhat sparse about it. There are no examples and the various TCA properties are only very briefly described. While I'll definitely improve the documentation, I thought I would already share with you some of my hard-earned wisdom. A word of caution: what you will find below may not be the whole story. It describes what I have understood so far and what worked for me. Don't hesitate to add your own knowledge to the pot.

I had the following scenario: a table that listed locations (in a city). Several other types of records could be related to these locations, for example to pages or news items. It was desirable – mostly for convenience – to be able to see from a locations record to what other records it was related. This is possible only with bidirectional MM-relations.

The main issue I encountered is that I expected the TCA definition to be symmetrical, since the MM-relation was supposed to be bidirectional. Matching TCA properties to this assumption proved impossible and left me stumped for a while. I had started out by defining a group-type field in the pages table to be able to select locations and a symmetrical group-type field in the locations table, but I couldn't find what to put in some of the TCA properties. Mostly I ended up with the "uid_local" and "uid_foreign" fields of the MM table inverted depending on which side of the relation I defined it.

After thinking some more, I realised that the pages table needed to be related only to the locations. It is on the other side that locations could be related to different tables. So the pages table could be extended with a select-type field pointing strictly to the locations table. So I ended up adding the following TCA to the pages table:

$newColumn = array( 
        'tx_myext_locations' => array(
                'label' => 'LLL:EXT:myext/locallang_db.xml:pages.tx_myext_locations',
                'config' => array(
                        'type' => 'select',
                        'foreign_table' => 'tx_locations',
                        'MM_opposite_field' => 'usage_mm',
                        'MM' => 'tx_locations_mm',
                        'MM_match_fields' => array(
                                'tablenames' => 'pages'
                        'size' => 5,
                        'maxitems' => 100
t3lib_extMgm::addTCAcolumns('pages', $newColumn, 1);
t3lib_extMgm::addToAllTCAtypes('pages', 'tx_myext_locations', '1');

The critical property here is "MM_opposite_field", which instructs TYPO3 that the relation is bidirectional and which field in the foreign table handles the other side of the relation. Furthermore "MM_match_fields" is used to force the "tablenames" field of the MM table (tx_locations_mm) to have the value "pages". The name "MM_match_fields" is slightly misleading. Based on the usage I explained, you would think it is more a default value than a kind of matching feature. The name makes sense when taking a different point of view: it is a filter instructing the TCA to display (in the MM-relations field) only those records whose "tablenames" field matches "pages", so that you only see relations to pages. Quite logically this same value is used to initialise the "tablenames" field when creating a new relation starting from the "pages" table.

Does that make sense? I hope so, so we can move on to the other side. Here's how the TCA looks like in the locations table:

$TCA['tx_locations']['columns']['usage_mm'] = array(
        'label' => 'LLL:EXT:myext/locallang_db.xml:tx_locations.usage_mm',
        'config' => array(
                'type' => 'group',
                'internal_type' => 'db',
                'allowed' => 'pages,tt_news',
                'prepend_tname' => 1,
                'size' => 5,
                'maxitems' => 100,
                'MM' => 'tx_locations_mm'

You can now rest your brain. As you can see, this side of the relationship contains nothing exceptional (or badly documented). Actually this is the main thing worth of notice: there's nothing indicating a bidirectional MM-relation on this side of the relation. It just looks like a perfectly normal group-type field with MM-relation activated. Just note the use of the "prepend_tname" property, which makes sure that the "tablenames" field is filled properly from this side too.

With this setup, the "uid_local" field will always contain keys from the locations table. The "uid_foreign" field will contain keys from whatever table is designated by the "tablenames" field.

I hope this will be helpful to some. If you have additional information or corrections, post them in the comments and I'll try to keep this article updated.


comment #1
Gravatar: Fabien Udriot Fabien Udriot June 10, 2009 16:55
It is helpful! Thanks for sharing. I faced this issue some time ago.

comment #2
Gravatar: Steffen Müller Steffen Müller June 10, 2009 22:55
Thanks a lot for this investigation. Your work on documentation is so valuable. Keep rolling!

comment #3
Gravatar: Maba Maba June 11, 2009 00:21
Thanks for your post. This nice feature was introduced in version 4.1 by Ingmar and Sebastian.
The possibility to create bidirectional relation for two records of the same table is still missing, as I remember. I also don't know anymore, if there was added an patch for managing the reference index correctly in the meanwhile. Perhaps I will check this the next days, when I would find my test extension, I've written two years ago. :-)

Sorry, comments are closed for this post.