Porting Flag to Drupal 8: Forming Maddness
I've been slowly plodding away on Flag since the water main was replaced back in mid-November. It's been difficult because work has been an exercise in frustration, home life has been stressful, and a multitude of other things that conspired to drain me of my available energy and concentration.
When it comes to Flag itself, the Drupal 8 version has long stopped being a mere port and is quickly transforming into a full-on rewrite. Drupal 8 is simply so different it's been easier for me to think of it as a new module effort rather than a update of the existing code. I've had to pull out large chunks of the existing code and slowly start folding it in piece by piece.
Flagging's Four
The biggest challenge was how to design the central code that constitutes a Flag. After numerous discussions on IRC wish some extraordinarily kind and patient core devs, we settled on the following:
- The Flag (the one the administrator creates) is a Configuration Entity.
- The Flagging (the one the user creates when they flag something) is a Content Entity.
- The Flag Type plugin is part of the Flag that provides any specific code needed to flag a particular kind of Drupal content.
- The Link Type plugin is a part of the Flag that provides the link the user clicks to flag a piece of content.
Configuration and Content entities are largely the same with two key differences: Configuration entities are saved to disk as YML files and are intended only to be created by site builders. Content entities are stored in the database, and are intended to be created by site users. To the module developer, they both act like key-value stores with a few standardized functions. Yes, they are objects in an OOP sense, but it acts like a general key-value store complete with generic get() and set() functions.
Building an entity object is actually not difficult. Define a few key methods, apply an @EntityType annotation, derive from either ConfigEntityBase or ContentEntityBase, and you're good to go. I haven't spent much time with content entities yet, so I may eat my words in my next post. For the most part, Drupal does a lot for you, but when you start getting Plugins involved, things get a bit tricky.
Plugging In, Bagging Up
Plugins are a great and terrible thing in Drupal 8. They provide a lot of easy expandability to a variety of use cases. The pattern is also so ubiquitous that one might over use it for the sake of simplicity and familiarity. For Flag, we have two plugins, a Flag Type and a Link Type.
While the Flag configuration entity represents the administrator-defined flag, the Flag Type provides the code that handles the relationship between the Flag and any piece of content you can flag in Drupal. Before Drupal 7, Flag only could handle three kinds of content: Users, nodes, and comments. Each required special code because they were all handled as standalone objects. In Drupal 7, the Flag module gained the ability to flag any entity. This opened up a lot of functionality, but there were still enough odd edge cases that we had separate handlers for users, comments, and nodes. Drupal 8 is expected to unify things further.
In theory, there shouldn't be a reason to even have other Flag Types. The only case would be if we want to flag something that's not an entity. For the moment, I have separate Flag Type plugins for users, comments, nodes, and one for entities. The latter acts as the base class for the former three. Furthermore, the entity Flag Type is a derivative. That is, it is a single class that uses configuration to create multiple instances of itself. In the future, there may only be a single Flag Type plugin, or I may remove the ability completely. Right now it makes sense to leave it in there just to be sure.
While the future of the Flag Type plugin is unclear, Link Types are certainly a necessity. The Link Type plugin handles the user interface attached to a flaggable object. Right now the link type isn't particularly useful yet as I've only defined one, non-functional plugin. At the moment, I'm only concerned with creating a Flag config entity, rather than the Flagging content entity and it's associated user interface code.
Both plugins are stored in the Flag entity in a plugin "bag". Why a bag and not just an array or a class variable? The reason is lazy loading. To reduce the memory footprint of the entity system, we only load the plugins when we need to use them. The bag provides this ability by communicating with a plugin "manager" that tells Drupal where to look for plugin classes, if a special annotation is involved, and so on. When someone requests a plugin from a bag, the bag pulls a string containing the plugin's ID from storage, then asks the plugin manager to instantiate the plugin using the ID. For Flag, we only need a single plugin instance per Flag config entity. For this reason, we use a special plugin bag -- DefaultSinglePluginBag -- that only stores a single plugin instance.
Forming Madness
Once I got the plugins and the bags squared away, the next problem was building a form to create new Flag entities. For a simple config entity, the process is straightforward. Create a new ListController to display all the entities. Create a new EntityFormController and define the buildForm() and save() methods. The FormAPI is more or less the same in Drupal 8 compared to Drupal 7. Since I was lost in 7, I'm still lost in 8 and rely heavily on the documentation.
Throwing plugins into the mix, however, makes things complicated. The Flag Type and Link Type plugins each have administrator-editable settings and should be presented in a form. To do that, however, I need to know what plugins the admin selected in the first place. To do this, I resorted to creating a FlagAddPageForm. This form does nothing but allows you to enter a few, very basic details about the flag. This includes the name, the kind of content it can flag (the Flag Type), how the flag link will respond (the Link Type), and the user roles that can use the flag. On submit, the form values are put in the new Drupal 8 TempStore API (a very big thank you to Tim Plunkett for that!) and the user is redirected to the Flag's EntityFormController. During the buildForm() step, the Flag Type and Link Type plugins are created, assigned to the Flag entity, and then asked to provide their piece of the Flag entity form.
Saving the entity is largely automatic again. Configuration and Content entity base classes contain a generic get() and set() method that enables it to create new class variables on demand. The new variables have the same as the field name in the entity form. The consequence is that when designing your entity classes, you need to choose variable names very carefully in order to avoid collisions. Furthermore, you need to choose which variables you want to be public or protected/private. Since the get() and set() methods are part of the entity's own base class, they naturally bypass the protected access modifier. Again, choose your field names very carefully!
Saving the Flag Type and Link Type plugin values turned out to be much simpler than I had thought. Since both the plugin classes implement PluginFormInterface, one only needs to call $plugin->submitConfigurationForm($form, $form_state) in the entity form controller's save(). The parameters are even passed by reference so as not to eat all the memory on the server. In order to save each plugin's configuration, however, we need to implement Flag::preSave(), which is called when saving the entity.
The preSave() method does a little bit of trickery in order to save the plugin configuration. It calls the getConfiguration() method of the Flag Type and Link Type plugins, then saves each as a class variable within Flag. This way, Drupal will serialize the entire Flag object, plugin configurations included for us.
What's next?
Right now, the Drupal 8 Flag module does make new configuration entities, and it *looks* like they have everything. The immediate problem is that I cannot edit them after creation. I'm unsure exactly what the problem is. Perhaps my trickery with the FlagAddPageForm has something to do with it. Once I sort that out, I plan to start work on the Flagging entity and it's user interface. Hopefully we'll have a barely functional module at the end of this process.