do_action and hook methods

Your analogy here is partially correct, but incomplete:

I understand that when do_action is called wp looks for all registered
listeners and invokes them according set priority.

You need to consider what happens when you call add_action. WordPress never searches for the add_action statements, it has those in a list. When you call add_action, it updates a list of callbacks to call when that action happens.

With this in mind, it’s more useful to use an events analogy.

  • do_action( 'init' ) Fire the ‘init’ action
  • add_action('init', 'myfunction' ) When the init event happens, call myfunction
  • function myfunction() {} Ah the init event happened

The events analogy helps enormously when understanding how actions and filters work, because there’s a whole class of problems that happen when you don’t understand related to timing.

Lets take your example of calling add_action in the footer for the wp_head action. wp_head fires when wp_head() is called, which should happen in the header between the <header> tags. If we try to add an action in the footer, then it will be added, and the next time wp_head() is called it will fire. But wp_head has already been called, it’s too late. It’s like asking a child “remember to pack your bag before the party” just after the party has ended.

With this in mind I have some general rules and recommendations that would act as heuristics for more maintainable code:

  • Put all code inside functions that fire on events. init, after_theme_setup, wp_head, and admin_init should cover most of it
  • The only code outside of a hook or filter Would be template files, and the initial add_action calls
  • Don’t add filters and hooks inside templates, do them in functions.php or a plugin, or if those files use include or require statements for an includes folder

There are some situations you’ll want to break those rules, usually to prevent recursion when saving things, etc, but those should be rare exceptions. Experiment and see what works, and look at this post that shows which actions fire when, and what’s safe to use at those times

A final note, actions and filters are the same thing. Internally they’re treated the same, but there are some important differences if you want things to work well:

  • Actions do things
  • Filters modify things
  • Actions don’t need to return anything
  • Filters are passed what’s being ‘filtered’ as the first argument, and always return it or a modified version
  • Filters are called a lot, so never do heavy work in filters, it will slow things down massively, only modify, doing work is what actions are for
  • Be wary of hooking into the translation and escaping filters, it can be dangerous, and has a major impact on performance as those are the most commonly called filters
  • There is an especially dangerous action/filter called all, that will fire for every filter and hook. Useful sometimes for debugging, but I’d recommend using a plugin such as query monitor instead.