Note:

If you want to create a new page for developers, you should create it on the Moodle Developer Resource site.

Hooks spec: Difference between revisions

From MoodleDocs
(Created page with "(15:23:58) Marina: I have some suggestion for 2.6 that actually can work well with logging. At the moment we have events system which is one-way - the core notifies whoever wants...")
 
 
(42 intermediate revisions by 7 users not shown)
Line 1: Line 1:
(15:23:58) Marina: I have some suggestion for 2.6 that actually can work well with logging. At the moment we have events system which is one-way - the core notifies whoever wants to listen to it about something that happens and it is proposed to be used for logging. But how about we implement also "feature" or "hook" system where some code in core may be substituted or extended with other plugins.
= Moodle hooks/features (Abandoned) =
(15:24:15) Marina: It is sort of mini-plugin system for all those small actions that don't need a separate plugin type. Examples where I can also see it useful - course search, global search, frontpage contents/redirection, etc.  
 
(15:24:38) Marina: Every time we need such sort of thing we introduce new $CFG variable and it's confusing and unmanageable
{{Note|This proposal was worked on for several years but no consensus was reached. A decision was made by the integrators to abandon this project as it has consumed too much of the development communities time and resources. The text on this page is kept for historical reasons only. See the comments on MDL-44078 for the full discussion.|gotcha}}
(15:24:58) Marina: There will be just a certain flexible interface and management page. Basically not so much from the core but plugins can go nuts.
 
(15:25:47) Martin Dougiamas: like wordpress and drupal?
If you are looking for the existing callback based API please see:
(15:25:52) Marina: yes
 
(15:26:22) Martin Dougiamas: it would be good, but I don't quit understand what "extend" code from core means
https://docs.moodle.org/dev/Callbacks
(15:26:28) Marina: I don't know about wordpress but in drupal it's not very manageable
 
(15:26:36) Marina: i.e. extend a form
 
(15:26:43) Marina: or contents of some page
 
(15:27:20) Martin Dougiamas: flexibility often is a tradeoff with usability
The full discussion is available on MDL-44078 and in forum: https://moodle.org/mod/forum/discuss.php?d=254508 and https://moodle.org/mod/forum/discuss.php?d=327349 but this issue is dead, please lets not keep discussing it.
(15:27:28) Martin Dougiamas: need some more fleshed out use cases with examples
 
(15:28:00) Martin Dougiamas: I don't understand your examples above
==Terms used==
(15:28:49) Marina: we have quite primitive course search algorithm in core. Some other plugin may implement search with relevance, analysing tags, going through attached pdf files, etc
 
(15:29:06) Marina: the same with global search
Terminology in the discussion around this feature has often been confusing because of the ways that other systems have used them (eg Drupal).
(15:30:11) Marina: 3. We have $CFG->customfrontpageinclude for frontpage. It could be also hook/feature
 
(15:30:31) Marina: - these are all examples of features where only one alternative implementation can be chosen
As a guide - the following terms are equivalent and any other interpretation is not allowed:
(15:30:59) Martin Dougiamas: Usually we just make new plugin types
 
(15:31:40) Marina: yes, or $CFG variables
* '''hook''' === hook point === trigger === caller
(15:32:08) Marina: ok, I'll try to find more examples and write here
* '''callback''' === listener === executor
(15:32:15) Martin Dougiamas: With your model, would it basically just replace X lines of code (producing HTML) with a given function which produces it's own Y lines of HTML connecting to it's own code?
 
(15:32:38) Marina: it's one of the use cases
== Goals ==
(15:32:45) Marina: but it does not have to be UI only
 
(15:33:17) Marina: in case of search - we ask plugin to give us list of courses and we go back to usual renderers to display them
# Create a consistent/simple/performant/secure way to create extension points where all plugins can run code (never specific plugin types) and modify data (no restrictions - not tied specifically to events)
(15:34:01) Martin Dougiamas: I like the idea if it doesn't complicate normal code too much, and the hooks across Moodle are sort of consistently managed
# Create a consistent/simple/performant/secure way to call code from another specific plugin
(15:34:29) Marina: yes, that's the main idea - to have the centralised management
# Allow implementations in an autoloaded class
(15:34:52) Marina: so admin can see the list of all available hooks and for each enable/disable available implementations
# Allow implementations in lib.php (only for legacy)
(15:35:22) Marina: we just query each plugin or core on definitions and implementations it has during upgrade/install only
# Allow testing hook implementations even if no plugin in core currently implements a callback
(15:35:34) Marina: and store the list in DB
# Enforce organisation and categorisation of all "callers" and "executors" in a plugin/subsystem/core
(15:35:53) Marina: so each time the hook needs to be called we include only necessary file
 
(15:36:34) Marina: I can write some pre-spec in more details if there is an interest
=== Disregarded Goals ===
This section is only kept to show the history of this issue - these goals were initially listed, but since then nobody requested and they seem like a bad idea (overly complex - no use case).
# Provide an admin UI to control the executed plugins for each hook (order and enable/disable)
# Dropping in a hook implementation from some other large framework/web app that does not suit Moodle
# Blowing this feature up into some full controller for business logic / dependency injection / bloat
 
== Current solutions ==
 
* Defining functions in /plugindir/lib.php with the name <pluginname>_<hookname> and querying all plugins if somebody implements function and/or list of plugins implementing it. *This is the current implementation of hooks.* Disadvantage: functions must be defined in lib.php therefore they are always included even when function is not there (performance loss); no UI to configure if only one plugin can implement feature; difficult for developers because they don't know if they can or how they can implement a hook (no separate place for documentation).
 
* Events system. The core notifies whoever wants to listen to it about something that happens. Disadvantage: one-way communication, no feedback. Advantage: plugins cannot break important core functionality.
 
* $CFG variables. These are fine if it makes sense to have a setting to choose different implementations of a feature (e.g. lock factories).
 
* Creating a new plugin type. Disadvantage: very long process, involves lots of documentation, creating lots of strings and UI, may be available in major releases only. Also not applicable for minor features. Forcing a "feature" to be split amongst many plugins.
 
* Renderers. These can be used to override a lot of standard Moodle behaviour, particularly on the view side. Only can be overridden by themes
 
== Implementation ==
 
* Hook definition is a class stored in /classes/hook/ folder (of core component or plugin). They have a function execute() that calls all registered callbacks and passes the actual instance of the hook definition class to them. Some hooks may be called on one component/plugin at a time only.
 
* Hooks to be executed must be registered in /db/hooks.php so it is possible to determine all the hooks called by a plugin/subsystem.
 
* Plugins that want to register hook executions (callbacks) do so in /db/hooks.php
 
* Hook manager, is responsible for caching of the list of existing hooks and their callbacks. It also has an interface with list of all hooks and callbacks.
 
=== Example ===
 
Typical example is a hook that allows plugins to add fields to the existing form. Note that actual hook classes may be much more complicated, with lots of methods that can be called from callbacks and also with properties/methods that can be called by the component who executed the hook to access the collected data from plugins.
 
0. Base class does almost nothing, it's just a suggestion for implementation, methods are not final (unlike events) '''/lib/classes/hook/base.php'''
<pre>
namespace core\hook;
abstract class base {
    private $isexecuting = false;
    public function execute($component = null) {
        if ($this->isexecuting) {
            throw new \moodle_exception('Already executing');
        }
        $this->isexecuting = true;
        \core\hook\manager::dispatch($this, $component);
        $this->isexecuting = false;
        return $this;
    }
}
</pre>
 
1. '''/lib/classes/hook/courseresetform.php'''
 
<pre>
namespace core\hook;
public class courseresetform extends \core\hook\base {
    private $mform;
    public function get_mform() {
        return $this->mform;
    }
    public static function create($mform) {
        $hook = new self();
        $hook->mform = $mform;
        return $hook;
    }
}
</pre>
 
2. '''/course/reset_form.php''', function course_reset_form::definition()
at the moment this function looks for function xxx_reset_course_form_definition() in all mod plugins and call this function passing $mform. We can have a legacy hook execution that does this for backward compatibility but it will just call:
<pre>
\core\hook\courseresetform::create($mform)->execute();
</pre>
The hook also must be listed in db/hooks.php
<pre>
// Hooks that exist in Moodle core.
$hooks = array(
    '\core\hook\courseresetform',          // Allows components/plugins to extend the course reset form.
);
</pre>
 
3. Now any plugin type and not only module but also blocks (existing issue MDL-24359) can register their callbacks in '''/blocks/myblock/db/hooks.php'''.
<pre>
$callbacks = array(
    array(
        'hookname'    => '\core\hook\courseresetform',
        'callback'    => array('block_myblock_hook_callbacks', 'courseresetform'),
        //'includefile' => '/optional/file/path.php',
    ),
);
</pre>
 
 
4. Actual callback implementation from /blocks/myblock/classes/hook_callbacks.php:
<pre>
class block_myblock_hook_callbacks {
    public static function courseresetform(\core\hook\courseresetform $courseresetformhook) {
        $courseresetformhook->get_mform()->addElement('checkbox', 'Reset contents of block Myblock');
    }
}
</pre>
 
== Use cases ==
 
# Navigation: we want to allow any plugin to put own items anywhere
# Recent activity in course
# Global search: similar to above but result will be list of title+url+abstract
# Front page: We have $CFG->customfrontpageinclude for including a file displaying frontpage contents. It could be also hook/feature
# Extending mimetypes list
# Extending licenses list
# Adding fields to the standard forms. This may be a comprehensive feature that includes several functions - adding fields, validation, processing.
 
== Comparison with Moodle Events ==
 
Differences with [[Events API]]:
* events are one way communication (from one to many) - hooks are two way communication (usually many to one)
* event data must not be modified - hook callbacks may alter the hook instance data
* events are triggered after actions - hooks can be executed at any useful stage (pre, post, validation, etc.)
* event structure is fixed - hooks can be arbitrary classes that may extend the base
* database transaction may cause event buffering - hook manager does does not consider transactions
* all events are logged (this is contentious) - hooks are not logged

Latest revision as of 11:22, 6 April 2020

Moodle hooks/features (Abandoned)

Note: This proposal was worked on for several years but no consensus was reached. A decision was made by the integrators to abandon this project as it has consumed too much of the development communities time and resources. The text on this page is kept for historical reasons only. See the comments on MDL-44078 for the full discussion.


If you are looking for the existing callback based API please see:

https://docs.moodle.org/dev/Callbacks


The full discussion is available on MDL-44078 and in forum: https://moodle.org/mod/forum/discuss.php?d=254508 and https://moodle.org/mod/forum/discuss.php?d=327349 but this issue is dead, please lets not keep discussing it.

Terms used

Terminology in the discussion around this feature has often been confusing because of the ways that other systems have used them (eg Drupal).

As a guide - the following terms are equivalent and any other interpretation is not allowed:

  • hook === hook point === trigger === caller
  • callback === listener === executor

Goals

  1. Create a consistent/simple/performant/secure way to create extension points where all plugins can run code (never specific plugin types) and modify data (no restrictions - not tied specifically to events)
  2. Create a consistent/simple/performant/secure way to call code from another specific plugin
  3. Allow implementations in an autoloaded class
  4. Allow implementations in lib.php (only for legacy)
  5. Allow testing hook implementations even if no plugin in core currently implements a callback
  6. Enforce organisation and categorisation of all "callers" and "executors" in a plugin/subsystem/core

Disregarded Goals

This section is only kept to show the history of this issue - these goals were initially listed, but since then nobody requested and they seem like a bad idea (overly complex - no use case).

  1. Provide an admin UI to control the executed plugins for each hook (order and enable/disable)
  2. Dropping in a hook implementation from some other large framework/web app that does not suit Moodle
  3. Blowing this feature up into some full controller for business logic / dependency injection / bloat

Current solutions

  • Defining functions in /plugindir/lib.php with the name <pluginname>_<hookname> and querying all plugins if somebody implements function and/or list of plugins implementing it. *This is the current implementation of hooks.* Disadvantage: functions must be defined in lib.php therefore they are always included even when function is not there (performance loss); no UI to configure if only one plugin can implement feature; difficult for developers because they don't know if they can or how they can implement a hook (no separate place for documentation).
  • Events system. The core notifies whoever wants to listen to it about something that happens. Disadvantage: one-way communication, no feedback. Advantage: plugins cannot break important core functionality.
  • $CFG variables. These are fine if it makes sense to have a setting to choose different implementations of a feature (e.g. lock factories).
  • Creating a new plugin type. Disadvantage: very long process, involves lots of documentation, creating lots of strings and UI, may be available in major releases only. Also not applicable for minor features. Forcing a "feature" to be split amongst many plugins.
  • Renderers. These can be used to override a lot of standard Moodle behaviour, particularly on the view side. Only can be overridden by themes

Implementation

  • Hook definition is a class stored in /classes/hook/ folder (of core component or plugin). They have a function execute() that calls all registered callbacks and passes the actual instance of the hook definition class to them. Some hooks may be called on one component/plugin at a time only.
  • Hooks to be executed must be registered in /db/hooks.php so it is possible to determine all the hooks called by a plugin/subsystem.
  • Plugins that want to register hook executions (callbacks) do so in /db/hooks.php
  • Hook manager, is responsible for caching of the list of existing hooks and their callbacks. It also has an interface with list of all hooks and callbacks.

Example

Typical example is a hook that allows plugins to add fields to the existing form. Note that actual hook classes may be much more complicated, with lots of methods that can be called from callbacks and also with properties/methods that can be called by the component who executed the hook to access the collected data from plugins.

0. Base class does almost nothing, it's just a suggestion for implementation, methods are not final (unlike events) /lib/classes/hook/base.php

namespace core\hook;
abstract class base {
    private $isexecuting = false;
    public function execute($component = null) {
        if ($this->isexecuting) {
            throw new \moodle_exception('Already executing');
        }
        $this->isexecuting = true;
        \core\hook\manager::dispatch($this, $component);
        $this->isexecuting = false;
        return $this;
    }
}

1. /lib/classes/hook/courseresetform.php

namespace core\hook;
public class courseresetform extends \core\hook\base {
    private $mform;
    public function get_mform() {
        return $this->mform;
    }
    public static function create($mform) {
        $hook = new self();
        $hook->mform = $mform;
        return $hook;
    }
}

2. /course/reset_form.php, function course_reset_form::definition() at the moment this function looks for function xxx_reset_course_form_definition() in all mod plugins and call this function passing $mform. We can have a legacy hook execution that does this for backward compatibility but it will just call:

\core\hook\courseresetform::create($mform)->execute();

The hook also must be listed in db/hooks.php

// Hooks that exist in Moodle core.
$hooks = array(
    '\core\hook\courseresetform',           // Allows components/plugins to extend the course reset form.
);

3. Now any plugin type and not only module but also blocks (existing issue MDL-24359) can register their callbacks in /blocks/myblock/db/hooks.php.

$callbacks = array(
    array(
        'hookname'    => '\core\hook\courseresetform',
        'callback'    => array('block_myblock_hook_callbacks', 'courseresetform'),
        //'includefile' => '/optional/file/path.php',
    ),
);


4. Actual callback implementation from /blocks/myblock/classes/hook_callbacks.php:

class block_myblock_hook_callbacks {
    public static function courseresetform(\core\hook\courseresetform $courseresetformhook) {
        $courseresetformhook->get_mform()->addElement('checkbox', 'Reset contents of block Myblock');
    }
}

Use cases

  1. Navigation: we want to allow any plugin to put own items anywhere
  2. Recent activity in course
  3. Global search: similar to above but result will be list of title+url+abstract
  4. Front page: We have $CFG->customfrontpageinclude for including a file displaying frontpage contents. It could be also hook/feature
  5. Extending mimetypes list
  6. Extending licenses list
  7. Adding fields to the standard forms. This may be a comprehensive feature that includes several functions - adding fields, validation, processing.

Comparison with Moodle Events

Differences with Events API:

  • events are one way communication (from one to many) - hooks are two way communication (usually many to one)
  • event data must not be modified - hook callbacks may alter the hook instance data
  • events are triggered after actions - hooks can be executed at any useful stage (pre, post, validation, etc.)
  • event structure is fixed - hooks can be arbitrary classes that may extend the base
  • database transaction may cause event buffering - hook manager does does not consider transactions
  • all events are logged (this is contentious) - hooks are not logged