Craft CMS Many to Many plugin

Peter Tell
Peter Tellposted on on October 2, 2014

So many relationships, so little time...

In the last post about the Craft CMS I mentioned that one of the concerns was the fact that relationships couldn’t be managed from both sides. The reason this came up was due to the fact that in upcoming client project they had made this exact request. They’ve got a relationship between two different types of content, and they want to manage that relationship from either end.

If that’s hard to follow think of Recipes that have Ingredients. Say you create a recipe called Chicken Noodle Soup. Part of the content is most likely going to be a list of ingredients. These ingredients should be managed somewhere else in the CMS so they can be reusable. If you need 1lb of butter, you shouldn’t have to keep adding 1lb of butter to the system each time. Also, because of the lack normalization, you’d have no way to aggregate these ingredients should you need to. There’s also huge room for data divergence. Scary.

<>In Craft, this is built in. I can simply create an entries field called Related Ingredients and limit it to the ingredients entry section and then add it to my recipe section. Fantastic. But what if I wanted to add recipes from the ingredients entry while keeping the relationships paired? Natively, you can’t. I mentioned this to my office mate Pat to which he replies, “Well… could a plugin handle that?”

Could a plugin handle that...

Turns out, a plugin could handle that. With some help from Brandon Kelly, the founder of Pixel & Tonic, I was guided towards the realization that all Craft’s relationships by default are already Many to Many relationships. There just wasn’t a way to manage them from both ends.

In traditional Many to Many pivot tables you’d have something like the following set of columns and records:

+----+---------------+-----------+
| id | ingredient_id | recipe_id |
+----+---------------+-----------+
| 1  | 1             | 1         |
+----+---------------+-----------+
| 2  | 1             | 2         |
+----+---------------+-----------+
| 3  | 1             | 3         |
+----+---------------+-----------+
| 4  | 2             | 2         |
+----+---------------+-----------+
| 5  | 2             | 2         |
+----+---------------+-----------+

Craft’s relations table doesn’t work quite like that which I infer are for a few reasons. They give precedence to the the initial “creator” of the relationship which they dub the “source”. The second member of the relationship is called the “target”. They do this because they allow sorting, multiple relationships, and locale based translations. All of these are related to the source. It looks a little like this:

+----+---------+----------+--------------+----------+-----------+
| id | fieldId | sourceId | sourceLocale | targetId | sortOrder |
+----+---------+----------+--------------+----------+-----------+
| 54 | 34      | 65       | NULL         | 67       | 1         |
+----+---------+----------+--------------+----------+-----------+
| 55 | 34      | 65       | NULL         | 68       | 2         |
+----+---------+----------+--------------+----------+-----------+
| 56 | 34      | 65       | NULL         | 69       | 3         |
+----+---------+----------+--------------+----------+-----------+
| 57 | 35      | 66       | NULL         | 67       | 1         |
+----+---------+----------+--------------+----------+-----------+
| 58 | 35      | 66       | NULL         | 68       | 2         |
+----+---------+----------+--------------+----------+-----------+

This also allows them to store all of their relationships in one table instead of having to create a separate table for each pairing.

Ok, so as you can see from this illustration we’ve got two source entries (65, 66) that have various relationships to targets (67, 68, 69).

This is the pivot table. We just need more information to create the relationship instead of just the name of both parties in the pairing. We need: fieldId, sourceId, sourceLocale, and targetId. Craft already manages the relationship from the first side, so we just need a way to get that information to our field type plugin.

Enter the settings

The way I solved this was using the Field Type’s settings. When you create a Many to Many field, you have to assign the Linked Section and the Associated Field. The Linked Section is the section where you created the initial relationship. In our example, this is Recipes. The Associated Field is the native Entries field type already built into Craft. This is where the initial relationships was created. This can get a little confusing, but in our example this would be a Field called Related Ingredients. Remember, we are associating ingredients to recipes initially.

So now, we’re armed with some info when the Many to Many field is added to a section. We know the source field (see the field Id column?), we’ll know the target because this is the actual entry you’ll be editing, we just need to know what the sources are. We can get this list of sources because in the settings we were told which section to look in. These sources come into play when a user tries to add relationships.

After this is, its just a matter of attaching the related entries to the DOM and looping through them on a POST request. With each entry that’s contained in our field’s POST params, we have that entry’s ID. This is the source (sourceId). So for each ID located we can check if the relationship already exists. This makes our unique relationship: sourceId + targetId + fieldId (+ sourceLocale). As of now the plugin doesn’t support locales, but if it did that would complete the unique relationship.

So now we can perform a database lookup and see if the relationship is there. If it already exists, we don’t have to do anything. If it doesn’t exist we must create it.

The last step is to record any entries that were removed from the relationships. I handle all of this through JavaScript as the user is interacting with the Entries.

These deleted entries also get stored in the POST params. To get rid of them its just a matter of looping through and using the same unique identifier (sourceId + targetId + fieldId (+ sourceLocale)) and removing the record.

Next steps

So, that’s the initial release of the plugin. I wouldn’t say it’s complete as there are definitely opportunities to make it more feature rich, but it works for the current needs. You can find out more information, documentation, and the plugin itself on Github: https://github.com/page-8/craft-manytomany

Wrap it up Pete

This is the first plugin I’ve developed for Craft and it was quite a joy to work with. The API is extremely clean, the documentation is good, and the support is great. The Stack Exchange site is a wonderful resource and P&T were extremely helpful for the various requests I had.