The flag base class's main entry point is the flag() method, which is multi-purpose: you use it to both flag and unflag, specifying the $action parameter.
This has a lot of machinery to verify access and suitability, and invoke hooks and rules and so on.
It also performs the actual work for flagging or unflagging, which consists of:
- changing the {flagging} table
- changing flag counts data
- talking to Field API
- invoking hooks
- invoking Rules
Some of this work is done in helper methods, and some in the flag() method. The helpers fall into several sets:
1. _increase_count() / _decrease_count()
2. _flag() / _unflag()
3. _insert_flagging() / _update_flagging() / _delete_flagging(), the 'entity CRUD methods'.
One thing that makes flag() messier than it should be is that the 3rd set, the entity CRUD methods, were added for Flag 3.x, while set 2 predates them. However, they really should be working together more closely. One example of awkward behaviour is the way that _flag() gets the new flagging ID for the record it saves, but only returns it rather than updates the $flagging record -- because it didn't get passed a flagging record, only the essential pieces of data.
I think the big picture here is that originally (I presume), flag() was meant to handle all the logic about figuring out what needed to be done, and whether it was permissible, while _flag() and _unflag() did the actual work. However, over the years and the versions, more has been tacked on: counts, hook invocations, Rules support, and most recently entity hooks and Field API.
I'm not sure what the best course of action is at this point.
My hunch is to move all the 'work' from flag() into the low-level helpers, probably _insert_flagging() / _update_flagging() / _delete_flagging(), and kill off _flag() and _unflag().
However, I think this should be a meta-issue, and we should make small incremental changes, partly to avoid breaking things, and also to get an idea of whether we're improving the readability of the code.
| Comment | File | Size | Author |
|---|---|---|---|
| #4 | 2202969.4.low-level-clean-up.patch | 28.19 KB | joachim |
| #3 | 2202969.low-level-clean-up-experimental.patch | 7.79 KB | joachim |
Comments
Comment #1
joachim commentedThis clean-up will help fix #2196055: entity hooks don't get passed a proper Flagging entity and #2196085: hook_entity_delete() should be invoked before field data is deleted.
Comment #2
joachim commentedThe more I think about this, the more I think the right approach is indeed this:
> My hunch is to move all the 'work' from flag() into the low-level helpers, probably _insert_flagging() / _update_flagging() / _delete_flagging(), and kill off _flag() and _unflag().
This would mean we separate the logic and validation and the prep work from that actual work of the DB changes, API calls, and hook invocations.
So we'd have:
Comment #3
joachim commentedHere's a rough stab. Needs further work. I should also have made this a sequential patch so it's clearer to see where the code is being moved around...
Comment #4
joachim commentedHere's a sequential patch that refactors the code and removes both sets of low-level methods in favour of a new single set.
With this, all the code for hook invocations, database operations, and cache clearing is together (apart from count changes).
It's now MUCH clearer to see what's going on and in what order. We can now clearly see problems like #2121053: field_attach_presave() is called too late.
So lots of bugfixes to do now that we have a clearer picture of the code flow here.
As for the count change code, I'm torn about folding that in. It does make sense, but I think it would tip flagging_insert() over into being too long on the page to comfortably take in. However, there are two things I can see that we could do to help with that:
- move the chunk of drupal_static_reset() calls to a helper method. I think that's useful code to wrap up, because it's perceived as a single block of 'clear lots of static caches' anyway. It's also a block that's repeated in two places, so that's a good thing too.
- add a helper to do rules_invoke_event_by_args() and the prep work. That would help readability: at the moment my eye just skims over the whole of that 'if (module_exists('rules'))' bit as just 'the Rules bit'.
So I think the roadmap now is take care of those two smaller refactorings in a follow-up, then consider moving the count change code in.
Comment #5
joachim commentedThis isn't meta any more, though it is the linchpin to a lot of other fixes and clean-ups.
Comment #6
socketwench commentedLooks good to me. I wish we could do more to refactor the top of that function, but I think that might actually distract from clarity at this point.
Comment #8
joachim commentedThanks for reviewing!
Committed with a non-fast-forward merge, so we get the step-by-step changes in the history.
> I wish we could do more to refactor the top of that function
Any bits in particular? The things that stand out for me:
- The getting / building of a $flagging looks a bit messy to me.
- if (!$skip_permission_check) { -- I generally think double negatives are bad, and when there is also an else{} block there is just no excuse for it!!
Comment #9
socketwench commentedMostly the function just feels too long for my taste. I prefer to keep a method no longer than a single screen and have it invoke private methods that correspond to major steps.
Comment #10
joachim commented> I prefer to keep a method no longer than a single screen
Agreed!
I definitely want to trim down flagging_insert() in that way: #2254171: move Rules event invocation to a helper method in flag_flag. I'll have a look at flag() and see if anything can be done there.
Comment #11
joachim commentedHmmm the only thing I can find is #2254399: remove pointless $account check. Other than that:
- the stuff with $skip_permission_check could be moved to a helper, but then that's a lot of nesting with access() which is bad too
- the stuff with getting a sid could be moved to a helper, but likewise there'd be more nesting
Also in both cases, we'd have to have the helper set $this->errors, and then flag() would have to go inspecting $this->errors to see whether it should stop proceedings if an error's been set. So to my mind we wouldn't gain much.
Fixing #2196055: entity hooks don't get passed a proper Flagging entity should involve a fair bit of refactoring of code around the creating/populating of the $flagging, which hopefully will reduce the code.