Modification Writing Basics

From Online Manual

Jump to: navigation, search

1. SQL Changes

As a general rule of thumb mods should not alter the name, size or index of any table which is created as part of an SMF default installation - unless of course it is a mod whose aim is to enhance performance in some way. Instead it is assumed that mods will generally only add new tables, or otherwise add new columns to existing tables. If it is essential for a modification to alter the default SMF structure then it must provide, as part of the uninstall script, a means to put the tables back to their original form. The reason behind this is to ensure that as the user upgrades SMF in the future, that upgrades do not fail as a result of your modification.

If you have added your own tables and columns it is probably inappropriate to remove this structure upon uninstallation of the mod - as all collected data will be lost. The reason behind this is simply that many users may uninstall a modification due to an upgrade, and be upset at the loss of data. Starting with SMF 2.0 a new <database> tag has been provided, that associated with the $smcFunc allows mod to add tables and columns when installed and provide a way to the admins to remove the data if they want during the uninstall.

2. Version Unspecific Mods

Using the package manager it is possible to define different installation methods dependant on the version of SMF someone is running. This makes it possible to have a mod which would install on (for example) SMF 1.0 and also install on SMF 1.1 Beta 2, even though the actual changes are different. This is achieved by adding a "for" attribute to the package-info.xml file included with all package manager mods. So, to define a set of install actions for SMF 1.0 you would use the install tag as:

<install for="SMF 1.0">

If an install tag is encountered without the "for" attribute then it will be followed regardless of the current version. SMF follows install tags like an if/else statement, it will look at each set of installation tags in turn until it finds one it matches. The last set of install tags in a package should always be left without a "for" attribute set. This ensures that if your package is used on a future version of SMF, then some installation actions will exist. This is best shown with an example, a basic example of a possible package structure, with most tags missing is:

<install for="SMF 1.0 Beta 6, SMF 1.0 Beta 6 Public">
  <modification file="mod_actions_b6.xml" />
</install>
<install for="SMF 1.0">
  <modification file="mod_actions_1_0.xml" />
</install>
<install>
  <modification file="mod_actions_1_1.xml" />
</install>

With a package like above, SMF will go through each set of tags, trying to find a set which match the current version. If by the last set of tags no version has been found, it will attempt to run the actions in the final set (Here mod_actions_1_1.xml). In this example that means anyone running SMF 1.1, 2.0 or 5.2 could still attempt to install the modification. Note exactly the same method is true for uninstall actions.

3. Minimizing Risk of Failure

Obviously as SMF continues to develop code will change, and mods will after time need to be updated to install correctly. However, if care is taken when defining what a modifications to look for, the chance of failure can be reduced - saving time for both yourself as new versions come out - and for users who find mods no longer install after an upgrade. The simplest way to reduce the risk of failure is to make the search string only as long as is needed. Say, for example, one has the following query:

      $select_columns = "
         IFNULL(lo.logTime, 0) AS isOnline, IFNULL(a.ID_ATTACH, 0) AS ID_ATTACH, a.filename, mem.signature,
         mem.personalText, mem.location, mem.gender, mem.avatar, mem.ID_MEMBER, mem.memberName, mem.realName,
         mem.emailAddress, mem.hideEmail, mem.dateRegistered, mem.websiteTitle, mem.websiteUrl, mem.birthdate,
         mem.location, mem.ICQ, mem.AIM, mem.YIM, mem.MSN, mem.posts, mem.lastLogin, mem.karmaGood, mem.karmaBad,
         mem.memberIP, mem.lngfile, mem.ID_GROUP, mem.ID_THEME, mem.buddy_list, mem.im_ignore_list, mem.im_email_notify,
         mem.timeOffset" . (!empty($modSettings['titlesEnable']) ? ', mem.usertitle' : '') . ", mem.timeFormat,
         mem.secretQuestion, mem.is_activated, mem.additionalGroups, mem.smileySet, mem.showOnline,
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce,
         mg.onlineColor AS member_group_color, IFNULL(mg.groupName, '') AS member_group,
         pg.onlineColor AS post_group_color, IFNULL(pg.groupName, '') AS post_group,
         IF((mem.ID_GROUP = 0 OR mg.stars = ''), pg.stars, mg.stars) AS stars";
      $select_tables = "
         LEFT JOIN {$db_prefix}log_online AS lo ON (lo.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}attachments AS a ON (a.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}membergroups AS pg ON (pg.ID_GROUP = mem.ID_POST_GROUP)
         LEFT JOIN {$db_prefix}membergroups AS mg ON (mg.ID_GROUP = mem.ID_GROUP)";

If the mod needs to change this query to also retrieve a new column in the members table called "custom", the mod may search for and replace the entire query:

<search for>
      $select_columns = "
         IFNULL(lo.logTime, 0) AS isOnline, IFNULL(a.ID_ATTACH, 0) AS ID_ATTACH, a.filename, mem.signature,
         mem.personalText, mem.location, mem.gender, mem.avatar, mem.ID_MEMBER, mem.memberName, mem.realName,
         mem.emailAddress, mem.hideEmail, mem.dateRegistered, mem.websiteTitle, mem.websiteUrl, mem.birthdate,
         mem.location, mem.ICQ, mem.AIM, mem.YIM, mem.MSN, mem.posts, mem.lastLogin, mem.karmaGood, mem.karmaBad,
         mem.memberIP, mem.lngfile, mem.ID_GROUP, mem.ID_THEME, mem.buddy_list, mem.im_ignore_list, mem.im_email_notify,
         mem.timeOffset" . (!empty($modSettings['titlesEnable']) ? ', mem.usertitle' : '') . ", mem.timeFormat,
         mem.secretQuestion, mem.is_activated, mem.additionalGroups, mem.smileySet, mem.showOnline,
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce,
         mg.onlineColor AS member_group_color, IFNULL(mg.groupName, '') AS member_group,
         pg.onlineColor AS post_group_color, IFNULL(pg.groupName, '') AS post_group,
         IF((mem.ID_GROUP = 0 OR mg.stars = ''), pg.stars, mg.stars) AS stars";
      $select_tables = "
         LEFT JOIN {$db_prefix}log_online AS lo ON (lo.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}attachments AS a ON (a.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}membergroups AS pg ON (pg.ID_GROUP = mem.ID_POST_GROUP)
         LEFT JOIN {$db_prefix}membergroups AS mg ON (mg.ID_GROUP = mem.ID_GROUP)";
</seach for>
<replace>
      $select_columns = "
         IFNULL(lo.logTime, 0) AS isOnline, IFNULL(a.ID_ATTACH, 0) AS ID_ATTACH, a.filename, mem.signature,
         mem.personalText, mem.location, mem.gender, mem.avatar, mem.ID_MEMBER, mem.memberName, mem.realName,
         mem.emailAddress, mem.hideEmail, mem.dateRegistered, mem.websiteTitle, mem.websiteUrl, mem.birthdate,
         mem.location, mem.ICQ, mem.AIM, mem.YIM, mem.MSN, mem.posts, mem.lastLogin, mem.karmaGood, mem.karmaBad,
         mem.memberIP, mem.lngfile, mem.ID_GROUP, mem.ID_THEME, mem.buddy_list, mem.im_ignore_list, mem.im_email_notify,
         mem.timeOffset" . (!empty($modSettings['titlesEnable']) ? ', mem.usertitle' : '') . ", mem.timeFormat,
         mem.secretQuestion, mem.is_activated, mem.additionalGroups, mem.smileySet, mem.showOnline,
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce, mem.custom,
         mg.onlineColor AS member_group_color, IFNULL(mg.groupName, '') AS member_group,
         pg.onlineColor AS post_group_color, IFNULL(pg.groupName, '') AS post_group,
         IF((mem.ID_GROUP = 0 OR mg.stars = ''), pg.stars, mg.stars) AS stars";
      $select_tables = "
         LEFT JOIN {$db_prefix}log_online AS lo ON (lo.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}attachments AS a ON (a.ID_MEMBER = mem.ID_MEMBER)
         LEFT JOIN {$db_prefix}membergroups AS pg ON (pg.ID_GROUP = mem.ID_POST_GROUP)
         LEFT JOIN {$db_prefix}membergroups AS mg ON (mg.ID_GROUP = mem.ID_GROUP)";
</replace>

However, this method has two problems. First of all, were this query to get modified in the future, the change would be erased. In addition, were another modification to modify the code prior to the installation of this mod, the search would fail. So, with that being said, how about instead searching for the smallest unique string possible?

<search for>
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce,
</search for>
<replace>
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce, mem.custom,
</replace>

That's better, there's much less chance of this failing in the future as SMF grows, but what would make this even better is if instead of replacing code we added code on a seperate line, like so:

<search for>
         mem.totalTimeLoggedIn, mem.ID_POST_GROUP, mem.notifyAnnouncements, mem.notifyOnce,
</search for>
<add after>
         mem.custom,
</add after>

Although not as nice to look at, the fact that this has not changed the original line means any other mod that wants to change this query can do the same thing, and this will never fail. Generally, using add before or add after will mean your modifications has less chance of going "Out of date".

Finally, whereever possible searching for anything version specific should definetly be avoided. If you want to add a string to Modifications.english.php, do not search for:

// Version: 1.1 Beta 1; Modifications
Instead use the xml attribute position="end" to add your entry to the end of the file:
<search position="end" />

And try to avoid to search for:

?>

And add your entries before it. This will avoid your modification being locked to that version number - and mean you do not have to edit your modification very time a new version comes out!