Skip to main content
Topic: A Function a Day (Read 17069 times) previous topic - next topic
0 Members and 1 Guest are viewing this topic.

A Function a Day

Let's see how long I can go. :P

One day, one function (or feature) explained, along with examples on how to use it in addons.

This is both a way of document more in detail functions and a way to "test" their real usability level (if I can't explain them, or you don't understand them, it means they are broken).

I'll use just one topic not to spread informations around, feel free to ask questions, post feedback or post your own function of the day if you want or I miss a day. ;)

List of functions
Last Edit: August 03, 2014, 07:50:18 pm by emanuele
Bugs creator.
Features destroyer.
Template killer.

elk_array_insert

Reply #1

Let's start with something already answered by Spuds here: elk_array_insert.

Like SMF, ElkArte bases many of the "structures" (menu, list of buttons (incidentally: heck the "message" buttons are still hard-coded into the template... meh), linktree),  on arrays.
Few months ago Arantor suggested something like that, at the time I was working on a clumsy class that later on I abandoned (even though the code is still there not used... I should just remove it) because after some feedback, it resulted in being too complex to use in the real-world.

elk_array_insert is a simple function that takes care of "insert" arrays into another one.
It can be used, for example, to add a new menu after or before another one.

Let's steal Spuds's example and let's adapt it.
Say we want to add a new menu entry to the main menu, after the personal messages.
We attach a function to the integrate_menu_buttons hook like that:
Code: [Select]
function integrate_menu_buttons_sauce(&$buttons, &$menu_count)
{
$insert_after = 'pm';

// Define the new menu item(s)
$new_menu = array(
'sauce' => array(
'title' => $txt['tasty'],
'href' => 'go here',
'show' => true
)
);

$buttons = elk_array_insert($buttons, $insert_after, $new_menu, 'after');
}
Done.

Three are the mandatory parameter the function expects:
1) the original array ($buttons in our case),
2) the key (a string) that identify the entry we want to use as a reference ('pm' in the example above),
3) our menu entry (usually another array).
Then there are other three optional parameters:
4) "where" that can be "before" or "after", if after the function will place the new entry after the one we specified in 2) (the key), otherwise it will be placed before it (the default is 'before'),
5) the fifth parameter is a boolean that describe if the original array (the one we passed as first parameter) is an associative array or not. This is important because it changes the way the position where the new entry will be inserted is identified. The default value is true, so if nothing is specified the function will handle the original array as an associative array. Menus are usually associative arrays (array('a_key' => 'something')), while for example the linktree is not (array('something')).
6) the "strict" parameter is used by array_search to make it search for identical elements (this means it will also check the types of the needle).

If you need to add something to a submenu, you have to "DIY":
Code: [Select]
function integrate_menu_buttons_sauce(&$buttons, &$menu_count)
{
$insert_after = 'calendar';

// Define the new menu item(s)
$new_menu = array(
'sauce' => array(
'title' => $txt['tasty'],
'href' => 'go here',
'show' => true
)
);

$buttons['home']['sub_buttons'] = elk_array_insert($buttons['home']['sub_buttons'], $insert_after, $new_menu, 'after');
}
Bugs creator.
Features destroyer.
Template killer.

createList - part 1

Reply #2

For today something that is not easy to describe in a single post: createList.

createList is a very, very useful function that does... create a list! YAY!
As you very well know, lists are present almost everywhere: even the messages in a topic are a list, the (list of) messages in a board, etc., but create a list is a lot of repetitive tasks:
  • grab the elements (possibly sorted in some way),
  • "process" them,
  • order them in columns,
  • create pages (if needed),
  • send them to a template.
And apart from few specific things the "basic" code is always the same.
And here it comes createList to help in a way: with that function the "only" thing you have to do to create a list of something is define an array of information, and no, I don't mean the data you have to display, but "how" you want to retrieve and display them.

Let's take a quick look at all (at least those documented) the options available in createList:
https://github.com/elkarte/Elkarte/wiki/Createlist

In a very short summary, in the array that you will pass to createList you define:
  • an unique identifier of the list,
  • the columns you want to display (this requires a bit more),
  • and a function (name or object and method) that will be called to retrieve the data,
and the list is ready.

Let's not continue without an example in front of us:
https://github.com/elkarte/Elkarte/blob/v1.0.0-beta.1/sources/admin/ManageAttachments.controller.php#L1392
This example is the declaration of the array of informations that will be later on passed to createrList. It is also moderately simple, there are the basics plus just a couple of extras here and there, so it is suitable for use in a tutorial like that one.

So, let's split it up into pieces to better analyse it, first chunk:
Code: [Select]
		$listOptions = array(
'id' => 'attach_paths',
'base_href' => $scripturl . '?action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id'],
'title' => $txt['attach_paths'],
'get_items' => array(
'function' => 'list_getAttachDirs',
),
Ohhh many interesting things.
First of all the unique id of the list is 'attach_paths', why this is important? Easy: because you can use createList multiple times in a page, and in order to distinguish the results you need something to identify them. But this is not the only reason, the id is also important because every time createList is called, the id is used to "generate" a hook:
Code: [Select]
function createList($listOptions)
{
call_integration_hook('integrate_' . $listOptions['id'], array($listOptions));
it's the first line of createList [1]. In our example then when createList is called, the hook integrate_attach_paths will be called. This is particularly important for addons, for two reasons: first because they can hook out of the box any list in ElkArte that uses createList, and second because any time they will use createList an hook will be called and other addons will be able to use it. At the moment, due to the bug and some limits the hook doesn't have "full and flexible powers" on the list, but this is something for the next time.

Second element of the array above is 'base_href', this is necessary in two cases:
  • when the list is spread across multiple pages
  • when the list can be sorted
in this case the declaration seems useless..., but let's not be too picky. :P
The base_href parameter is simply the common, base, part of the url that will be used to create the links in the headers for sorting columns or to pass from one page to the other.

Next one if 'title', well, easy enough: an header (h3 in the common template) that will be displayed above the list.

Finally the most important 'get_items' this array, in this simple form, contains only the name of the function that will be called to retrieve the elements of the list, in that case list_getAttachDirs[2].

Okay, next chunk:
Code: [Select]
			'columns' => array(
'current_dir' => array(
'header' => array(
'value' => $txt['attach_current'],
'class' => 'centertext',
),
'data' => array(
'function' => create_function('$rowData', '
return \'<input type="radio" name="current_dir" value="\' . $rowData[\'id\'] . \'" \' . ($rowData[\'current\'] ? \' checked="checked"\' : \'\') . (!empty($rowData[\'disable_current\']) ? \' disabled="disabled"\' : \'\') . \' class="input_radio" />\';
'),
'style' => 'width: 10%;',
'class' => 'centertext',
),
),
'path' => array(
'header' => array(
'value' => $txt['attach_path'],
),
'data' => array(
'function' => create_function('$rowData', '
return \'<input type="hidden" name="dirs[\' . $rowData[\'id\'] . \']" value="\' . $rowData[\'path\'] . \'" /><input type="text" size="40" name="dirs[\' . $rowData[\'id\'] . \']" value="\' . $rowData[\'path\'] . \'"\' . (!empty($rowData[\'disable_base_dir\']) ? \' disabled="disabled"\' : \'\') . \' class="input_text"/>\';
'),
'style' => 'width: 40%;',
),
),
'current_size' => array(
'header' => array(
'value' => $txt['attach_current_size'],
),
'data' => array(
'db' => 'current_size',
'style' => 'width: 15%;',
),
),
'num_files' => array(
'header' => array(
'value' => $txt['attach_num_files'],
),
'data' => array(
'db' => 'num_files',
'style' => 'width: 15%;',
),
),
'status' => array(
'header' => array(
'value' => $txt['attach_dir_status'],
'class' => 'centertext',
),
'data' => array(
'db' => 'status',
'style' => 'width: 25%;',
'class' => 'centertext',
),
),
),
BAM!
Columns.
In this central part we are defining what columns we want to be able to see in our list, the way to define a column can have several variants, the the two mandatory parts are the header and "how to show the data" that can be one of several elements[3].
First off let's remember that this part is "working" with the array of values returned by the 'get_items' function, this may become more clear as we go.
'header' usually contains a 'value' that is a simple text string used as header of a column, in some of the above columns there is also a 'class', this si a css class that will be attached to the header of the column to style it[4].
Now the most important part: 'data', this is an associative array that defines the content and style of each and every cell of our list/table. There are several way pick the "value" we want to display, in the example above only two of them are used, so let's just stick with those (all the others are explained in the documentation):
  • 'db', is the easiest way to grab something to display[5]in createList, it just pick the corresponding index from the array returned by the 'get_items' function
  • 'function' this instead is the most flexible way to deal with data, it is a function that is executed for each entry in the corresponding column. The function receives as argument the full row of data each time. The function shall return a string that is what will be used to populate the cell.

I think that for this time there is already enough informations floating around, so let's just skim through the last bits:
Code: [Select]
			'form' => array(
'href' => $scripturl . '?action=admin;area=manageattachments;sa=attachpaths;' . $context['session_var'] . '=' . $context['session_id'],
),
'additional_rows' => array(
array(
'position' => 'below_table_data',
'value' => '
<input type="hidden" name="' . $context['session_var'] . '" value="' . $context['session_id'] . '" />
<input type="submit" name="save" value="' . $txt['save'] . '" class="right_submit" />
<input type="submit" name="new_path" value="' . $txt['attach_add_path'] . '" class="right_submit" />',
),
empty($errors['dir']) ? array(
'position' => 'top_of_list',
'value' => $txt['attach_dir_desc'],
'style' => 'padding: 5px 10px;',
'class' => 'windowbg2 smalltext'
) : array(
'position' => 'top_of_list',
'value' => $txt['attach_dir_save_problem'] . '<br />' . implode('<br />', $errors['dir']),
'style' => 'padding-left: 35px;',
'class' => 'warningbox',
),
),
);
'form' this can be used to "surround" the list with a form (so that you can let people input things and then submit them as a normal html form).
'additional_rows' is an array of elements that can be positioned in several different places before and after the list itself, they are frequently used to add buttons or legends or other elements necessary to the list.

Once the list is created, where does the data go?
There are two possible ways to render a list:
  • using a sub_template that directly handles the array generated by the list
  • using the default template_show_list template
    In this second case, there are again two possibilities:
    • use template_show_list as a sub_template
    • call it from another sub_template
Let's forget for the moment about 1, and let's focus on 2. In order to use template_show_list as a sub_template, we have to do another "trick" before:
Code: [Select]
$context['default_list'] = 'attach_paths';
we have to define which one is the "default list" to show.
Then we can define the sub_template as usual:
Code: [Select]
$context['sub_template'] = 'show_list';
and it's done.
The alternative 2b is used for example when you want to have multiple lists in the same page, in that case you can define your own sub_template like:
Code: [Select]
$context['sub_template'] = 'my_own_list';
Then you will create the template like:
Code: [Select]
function template_my_own_list()
{
    template_show_list('attach_paths');
}
you will pass the id of the list you want to display as argument of template_show_list and the generic template will render your list for you!

This is much more than I wanted to write... :-\ Oh well, next one will be short. :P
and there is a bug, because $listOptions should be preceded by &. Documenting things is good to spot bugs as well!
You can see this function in Attachments.subs.php
here becomes a bit difficult to explain, so do not hesitate if the text becomes messy, ask me to clarify ;)
As usual there is much more about classes and ids in the markup, but is food for the next round
in fact is a bit more complex, but let's use the simplified version for the moment
Bugs creator.
Features destroyer.
Template killer.

database

Reply #3

Today I'm a bit busy, so a very simple one: database.

This function does one single thing in only one way: it retrieves and return a database object. Stop.

Example:
Code: [Select]
$db = database();
done.

In the old SMF world, there was the famous $smcFunc to add to the global declaration and then use things like
Code: [Select]
$smcFunc['db_query'](blabla)

in Elk's world the way to use the database is:
Code: [Select]
// retrieve the db object
$db = database();

// use it
$db->query(blabla);
query and the rest of the db layer are for another day.
Bugs creator.
Features destroyer.
Template killer.

addMembersToGroup

Reply #4

Today I'm quite late... sorry, I lost a day (I thought it was Sunday...)

Today's function is an administrative one that may become handy for addons: addMembersToGroup
Let's see what the documentation says (that way we can also start to familiarize with docBlocks:
Code: [Select]
 * Add one or more members to a membergroup.
 *
 * Requires the manage_membergroups permission.
 * Function has protection against adding members to implicit groups.
 * Non-admins cannot add members to the admin group, or protected groups.
 *
 * @param string|array $members
 * @param int $group
 * @param string $type = 'auto' specifies whether the group is added as primary or as additional group.
 * Supported types:
 *  - only_primary    - Assigns a membergroup as primary membergroup, but only
 *                      if a member has not yet a primary membergroup assigned,
 *                      unless the member is already part of the membergroup.
 *  - only_additional - Assigns a membergroup to the additional membergroups,
 *                      unless the member is already part of the membergroup.
 *  - force_primary   - Assigns a membergroup as primary membergroup no matter
 *                      what the previous primary membergroup was.
 *  - auto            - Assigns a membergroup to the primary group if it's still
 *                      available. If not, assign it to the additional group.
 * @param bool $permissionCheckDone = false if true, it checks permission of the current user to add groups ('manage_membergroups')
 *
 * @return boolean success or failure

The function resides in Membergroups.subs.php, as written in the documentation its basic function is just add one or more members to a specific member group.
The basic usage is rather simple:
Code: [Select]
addMembersToGroup(10, 1);
This code adds the member with ID 10 to the group 1 (the administrators group). It will also "decides" whether the group will be primary or additional all by itself. This provided that the user "running" the function is allowed to change membergroups.

Then of course few parameters can be specified in order to better tune the usage, for example you can specify an array of members, or if the group should be primary or additional, and if you want to override the permissions.
This last part (about permissions) is nice, because for example we could use it to change a membergroup of a member based on some action (for example a subscription <= hit we are not yet doing it, but we are using custom queries! :P), even if the member is technically not allowed to change groups.

So, let's say we create a kind of reputation system that when a member reach 100 points he is inserted automatically into a special group, we could write:
Code: [Select]
require_once(SUBSDIR . '/Membergroups.subs.php');
addMembersToGroup($user, 15, 'only_additional', true);
Done. the function will take care of move the user $user to the group 15, making it an additional group, and without checking permissions, because the code would run at "any time", so the user triggering that function could not be an admin (or alike).
Bugs creator.
Features destroyer.
Template killer.

template_select_boards

Reply #5

Today a template helper function: template_select_boards.

This function is stored in the GenericHelpers.template.php file (under themes/default/ of course) and it does the simple task of render a select box (<select>) with a list of boards grouped by categories.
The function goes hand in hand with getBoardList that I was thinking to review today, but since I just proposed a change I thought it's better to wait a day or two and describe it in its final form, so I'll start from the end. :P

Normally getBoardList retrieves a list of boards subdivided by categories and then template_select_boards renders them in an almost completely configurable select box.
Let's have a look at the function fingerprint (i.e. the list of arguments accepted):
Code: [Select]
function template_select_boards($name, $label = '', $extra = '', $all = false)
  • the first $name is the name we want to give to the select (and it will be also the id) and is the only mandatory argument;
  • $label is a text that we want to use as... label of the select;
  • $extra is anything we want to append to the <select> tag (for example javascript or microdata or anything else).
    The final structure will look like:
    Code: [Select]
    <label>$label</label><select name="$name" id="$name" $extra><option>....
  • $all, the last argument, is a boolean and if true will show as first option the text: "Available in all boards".

The function is already used in several places across Elk's code, for example when posting an event, or when moving a topic (the select "Move to:") or when quoting a message to a new topic to pick a board where you want to start the new topic, etc.

This should help both theme and addon creators: the firsts would be able to customize it in a central place (for example replacing it with an "html" form to better style it or something), the seconds would be able to write less code and give it a common appearance across all the forum.
Bugs creator.
Features destroyer.
Template killer.

loadCSSFile

Reply #6

Today I'll start a series of 4 "helper" functions that are all similar and so it's nice to do them one after the other.

The first (in the order of appearance in Load.php :P) is loadCSSFile.

Warning: in order to describe these three functions I'll need to do take a detour and explain few unrelated things in this first post.

The name of ElkArte functions are all rather explicative (usually) and as such this function name explains exactly what the function does: it loads a CSS file.
This is the DocBlock and function fingerprint:
Code: [Select]
/**
 * Add a CSS file for output later
 *
 * @param string $filename
 * @param array $params = array()
 * Keys are the following:
 *  - ['local'] (true/false): define if the file is local
 *  - ['fallback'] (true/false): if false  will attempt to load the file from the default theme if not found in the current theme
 *  - ['stale'] (true/false/string): if true or null, use cache stale, false do not, or used a supplied string
 *
 * @param string $id = ''
 */
function loadCSSFile($filenames, $params = array(), $id = '')

[detour starts]
Under spoiler so that if you want to skip it it's easy. ;)

Why would we want to have a function that loads css file? And what does it mean "load a css file"?
Spoiler (click to show/hide)
[detour ends]

And let's have a look at how to use the function:
Code: [Select]
loadCSSFile('my_own.css');
Done.
The file my_own.css will be added to the template.
The function is configured to automatically check if the file exists in the "current theme" directory and if not fallback to the one present in the default directory.

The function can also be used to load remotely hosted files, for example:
Code: [Select]
loadCSSFile('http://myotherdomain.tld/somedir/file.css');

It is also possible to specify more than one file at a time:
Code: [Select]
loadCSSFile(array(
    'my_own.css',
    'http://myotherdomain.tld/somedir/file.css'
));

The parameters allow to tune some options, but in general are best left empty unless you badly need any of them, so I'm not going to explain what they do. :P
Bugs creator.
Features destroyer.
Template killer.

loadJavascriptFile

Reply #7

Let's continue the series with loadJavascriptFile.
The function is almost exactly the same as the previous loadCSSFile: it just loads into the <head> a javascript file instead of a css file.

Code: [Select]
/**
* Add a Javascript file for output later
*
* Can be passed an array of filenames, all which will have the same parameters applied, if you
* need specific parameters on a per file basis, call it multiple times
*
* @param array $filenames
* @param array $params = array()
* Keys are the following:
* - ['local'] (true/false): define if the file is local
* - ['defer'] (true/false): define if the file should load in <head> or before the closing <html> tag
* - ['fallback'] (true/false): if true will attempt to load the file from the default theme if not found in the current
* - ['async'] (true/false): if the script should be loaded asynchronously (HTML5)
* - ['stale'] (true/false/string): if true or null, use cache stale, false do not, or used a supplied string
*
* @param string $id = ''
*/
function loadJavascriptFile($filenames, $params = array(), $id = '')

In comparison to loadCSSFile, there are a couple of options more:
  • defer that if set to true will add the file to the end of the page instead of to the <head> tag, and
  • async that implements the HTML5 attribute.
    Everything else is the same.
Bugs creator.
Features destroyer.
Template killer.

addInlineJavascript

Reply #8

Javascript is not only files. Javascript can be also inline scripts!
And for that reason ElkArte has: addInlineJavascript!
The main advantage of this function is to help keep all the bits of scripts that usually are spread across the page in one place... well, actually in two possible places: in the header or just before the closing body tag.

Documentation:
Code: [Select]
/**
 * Add a block of inline Javascript code to be executed later
 *
 * - only use this if you have to, generally external JS files are better, but for very small scripts
 *   or for scripts that require help from PHP/whatever, this can be useful.
 * - all code added with this function is added to the same <script> tag so do make sure your JS is clean!
 *
 * @param string $javascript
 * @param bool $defer = false, define if the script should load in <head> or before the closing <html> tag
 */
function addInlineJavascript($javascript, $defer = false)

And example:
Code: [Select]
addInlineJavascript('
$(document).ready(function() {
$().linkifyvideo(oEmbedtext);
});');
So, the param is a simple piece of javascript, nothing more.
This example is taken from Display.php and is the code that takes care of embed videos.
This piece of code will be placed in the <head> tag at the top of the page.

If we wanted to have it at the end of the page we could have changed it to:
Code: [Select]
addInlineJavascript('
$(document).ready(function() {
$().linkifyvideo(oEmbedtext);
});', true);
The true on the last line tells ElkArte not to put the piece of script in the header, but just before the closing <body> tag (the function documentation is broken :P).
Bugs creator.
Features destroyer.
Template killer.

addJavascriptVar

Reply #9

And the last function of the round: addJavascriptVar.

Again another easy to guess function: adds one or more javascript variables.

Code: [Select]
/**
* Add a Javascript variable for output later (for feeding text strings and similar to JS)
* Cleaner and easier (for modders) than to use the function below.
*
* @param string $key
* @param string $value
* @param bool $escape = false, whether or not to escape the value
*/
function addJavascriptVar($vars, $escape = false)
The DocBlock is actually wrong... well, old. :P

It is frequently useful if you want to assign for example text strings to a javascript variable and use the localized string later in js, an example taken from the code:
Code: [Select]
			addJavascriptVar(array(
'poll_remove' => $txt['poll_remove'],
'poll_add' => $txt['add_poll']), true);
The first argument is an array of key/value pairs where the key will become the name of the variable and the value will be the... well, the value of the javascript variable.
The second argument is a boolean, if true the value will be passed to JavaScriptEscape, otherwise it will be output without escaping.

The code above will output something like:
Code: [Select]
var poll_remove = 'Remove poll',
    poll_add = 'Add poll';

If the "true" were omitted the output would have been:
Code: [Select]
var poll_remove = Remove poll,
    poll_add = Add poll;
Obviously wrong. ;)
Bugs creator.
Features destroyer.
Template killer.

determineAvatar

Reply #10

After about 8 months, I'm back! :P

Today's function is determineAvatar.

Find the avatar of a user may look like a simple thing, but it's not.
ElkArte allows for several different type of avatars:
  • Uploaded avatars as attachments,
  • Uploaded avatar in a custom directory,
  • External URL,
  • Default avatar,
  • Gravatar
I may have missed some combination. lol

So, in order to fine a member's avatar the common operation is a query that JOINs the attachments table on the member's ID and the result are 5 values that have to be combined together in order to obtain a single URL. Not something trivial.
Additionally, there is some legacy stuff for avatar resizing that is still around, and should be taken into account.

The function I'm presenting today, does exactly that: provided an array with these 5 "elements" it returns the url to the member's avatar.
The 5 values are the following database fields ({table}.field):
  • {members}.avatar - can be an url or the type of avatar
  • {members}.email_address - used for the gravatars
  • {attachments}.id_attach - of course the id of an attachment that should represent the member avatar if saved as attachment
  • {attachments}.attachment_type - type of attachment, if empty means avatars as attachment, otherwise it means the avatars are stored in another directory and have their own URL,
  • {attachments}.filename - the name of the file (of the avatar obviously :P).

So, a query to grab what you need would require at least something like this:
Code: [Select]
		SELECT
[..]
mem.avatar, mem.email_address,
IFNULL(a.id_attach, 0) AS id_attach, a.filename, a.attachment_type
FROM [...]
LEFT JOIN {db_prefix}members AS mem ON (mtn.id_member_from = mem.id_member)
LEFT JOIN {db_prefix}attachments AS a ON (a.id_member = mem.id_member)
The result of this query (fetched with fetch_assoc) would be an array containing the values of the fields, let's say $row.
Use determine Avatar is as easy as to call it:
Code: [Select]
$avatar_url = determineAvatar($row);
That's all you have to do.

determineAvatar returns an array of 5 element:
Code: [Select]
$avatar = array(
'name' => '',
'image' => '',
'href' => '',
'url' => ''
'gravatar_preview' => ''
);
image is the img tag.
href and url are almost exactly the same, except that href is always filled, while url doesn't contain anything in certain cases[1].
gravatar_preview is always present, even if there is no avatar set.

That's a quick description, though I hope it may help![2]
I actually wonder why...
For sure it helped me remember that the return of the function is an array and not an url... lol
Bugs creator.
Features destroyer.
Template killer.

 

Modules and events - Elk 1.1

Reply #11

This is a slightly more "wide" explanation for today.

Recently I introduced a couple new ways to expand ElkArte that will make their appearance starting from version 1.1, but I wanted to start introducing them now, because they give a bit more power to coders that want to write addons.
These "things" are not really finalized yet, they may change before the final release, but the general idea is present, so today I'll start with that and with hopefully a couple of example to show how to use them.

The first thing I wanted to talk about are modules, but then I realized that modules can express their full potential only using events, so I decided to take a step back and start with events.

Since have a working example to read is important, I'll use the calendar code that has been extracted from the core and converted to a module in the development branch.

Starting from Elk 1.1, each controller will be able to "fire" events. At first sight, an event is not much different from a hook, let's have a look at the BoardIndex_Controller class, we will find something like:
Code: [Select]
$this->_events->trigger('pre_load', array('boardIndexOptions' => &$boardIndexOptions));
so, an identifier "pre_load", and an array of variables, similar to what you see for a random hook:
Code: [Select]
call_integration_hook('integrate_validateSession', array(&$types));

So, why did I introduce events? Because I'm mad... oh, no way that was supposed to be in my head. :P
No, there are couple of reasons that I dare to think are also advantages.

First advantage
Objects using events are able to live for the entire time the controller is alive.
A typical hook call is "live" only for the time of the call, unless you use $context or other globals, you are not able to share variables from one hook call to the other.
For the way events are built, instead, the "object" reacting to an event will be kept alive and will be able to react to other events as well, giving you the possibility to share code (usually variables) between different positions in the controller without having to pollute the global scope and without the risk another addon will conflict with your code. Unfortunately, at that moment I don't have any good example for this, so I'll resort to some pseudo code:
Code: [Select]
<?php
class Calendar_Post_Controller
{
    protected $state = 0;

    public function pre_load()
    {
        $this->state = 1;
    }

    public function post_load()
    {
        if ($this->state == 1)
        {
            // Do something
        }
    }
}
Assuming pre_load and post_load are two triggers, to do something like that with normal hooks you'd have to use $context:
Code: [Select]
<?php
function calendar_pre_load()
{
    global $context;

    $context['calendar_state'] = 1;
}

function calendar_post_load()
{
    global $context;

    if ($context['calendar_state'] == 1)
    {
        // Do something
    }
}

Second advantage
You can "ask" for dependencies (in the form of variables) the controller.
Hooks are able to pass variables, but you have to take what they offer and you cannot do anything more.
With events, you will be able to ask the controller to pass you or not a wide range of variables:
variables normally passed by the trigger,
public or protected properties of the controller,
* global variables.
And you will be able to get them in the order you prefer.

Following the Calendar example, in Display_Controller, we need to react to the topcinfo event at:
Code: [Select]
$this->_events->trigger('topicinfo', array('topicinfo' => &$topicinfo));
this event by default passes only the $topicInfo variable, but in the Calendar_Display_Module we need more, we need also to know the topic, so writing the appropriated code we will be able to make the "Event_Manager"[1] pass us the $topic variable writing something like:
Code: [Select]
array('blablabla', array('topicinfo', 'topic')),
the Event_Manage will take care of searching for what we want and will send it to our module.
A similar call will be associated to a method (a function of a class) with the following structure:
Code: [Select]
    public function myfunction($topicInfo, $topic)
alternatively we could write:
Code: [Select]
array('blablabla', array('topic', 'topicinfo')),
that will be used in association to a function:
Code: [Select]
    public function myfunction($topic, $topicInfo)
As you may have guessed, in order to request a dependency, you have to know its name.

This feature is of particular importance, because it basically allows you to extend the controller almost the same way if you were to edit its code, except for passing local variables not listed in those passed by the trigger, but I'm not even sure this is possible.

Next round I'll move explaining a bit the concept of "module" and finally I'll merge the two giving you the full (blurry) picture.
Event_Manager is the class that handles all the events in a controller and all the listeners to these events.
I know I'm throwing in some random names and concepts, explain all of them in details would take much more and would bring us far away from the main topic of the post, so feel free to ask questions. ;)
Last Edit: November 01, 2017, 02:30:21 pm by emanuele
Bugs creator.
Features destroyer.
Template killer.

Re: A Function a Day

Reply #12

Thanks for all the documentation! I don't understand a word of it but what you guys do for us is really appreciated!  :)

Re: A Function a Day

Reply #13

lol
Bugs creator.
Features destroyer.
Template killer.

Modules and events (part 2) - Elk 1.1

Reply #14

In the first part I talked about events, now it's time to give an idea of what a "module" is instead.

Modules... modules are just a name I made up to define a system that automagically loads stuff and should make easier and more consistent the code organization.

Let's start from the original idea: I wanted to build a system able to scan the file system, find files with a certain name and load the classes in there, attaching hooks "on-the-fly" without having to pass through the installation procedure.
After a first try I decided that scan the file system may or may not be the best way, for a start it may be slow, and, more importantly, it may not have a way to disable it, except delete the file[1].
So I went a step back and made the system a little less flexible, but also nicer (I think).

ElkArte (starting from version 1.1) is able to "discover" module in the admin panel.
Modules are discovered automatically when a class with the appropriate name, stored in an appropriate file is present either in sources/admin/ or sources/addons/{subdir}/
The naming pattern is:
Code: [Select]
Class: Manage{Nameofthemodule}_Controller
File: Manage{Nameofthemodule}Module.controller.php

For example, the calendar has ManageCalendarModule_Controller in the file ManageCalendarModule.controller.php.
With that combination ElkArte is able to find it in the admin panel and show a button... drum roll ... in the Core Features page! :P[2]
So, we create a file and a class and ElkArte is able to show us a button to enable the functionality directly in the admin panel. Isn't it cool? 8)

Well, it's not yet so easy, there is still something to do manually: add the button to the core features page. Yes, this is actually a design choice (for the moment), because modules are used to extend several controllers at different levels of complexity and as such it may not be necessary to add an entry to the core features page[3].
In order to add an entry to the Core Features page, the Mange*Module_Controller class needs a static method called addCoreFeature accepting the $core_features array and adding the new entry to it, see the ManageCalendarModule_Controller as a reference:
https://github.com/elkarte/Elkarte/blob/8e042deefddd970a3e4ba120cb136845adbbb372/sources/admin/ManageCalendarModule.controller.php#L37
Then, in the setting_callback parameter of the Core Features button, we can proceed to enable (or disable) the modules when the button is pressed.
To enable a module we can use the enableModules function, that accepts two parameters: a string with the name of the module to enable, and an array with the name of the controllers the module will interact with. As an example:
Code: [Select]
enableModules('calendar', array('post', 'boardindex'));
to disable, same thing, just using the function disableModules.

Okay, for today that's all, it's late and the argument is quite tricky (even though explain it is more difficult than just reading the code), so I take a break and go bed.
Or each addon would have to invent a method to check if the module is enabled or not and replicate it in all the addons leading to a mess of checks.
I just finished to watch Alice in Wonderland, you can't expect me to be serious, sorry. :P
There is an "auto-add", but it's for "integrations", that is something I'll explain later... darn, I added too many things at once this time. LOL
Luckily Norv is no more around so I can destroy everything without fear. :P
Bugs creator.
Features destroyer.
Template killer.