This article may have been copied from the the forum-based Online Manual and contain some HTML remnants that need to be cleaned up. Please clean it up. This template may be removed when cleanup is complete. |
Overview to the Coding Guidelines:
[" class="bbc_link">Layout Related Guidelines] | | | [" class="bbc_link">Code Related Guidelines] | | | [" class="bbc_link">Mod(ification) Writing Basics] |
----
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. If you are determined to remove the structure upon uninstallation then please do so providing a seperate package so the user is fully aware of what is being carried out in their database.
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 encounted 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. Minimising 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 modificationis 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 I have this query below:
$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)";
And I need my mod to change this query to also retrieve a new column I added to the members table called 'custom'. I could do a search on the whole query as below:
<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 has two problems. Firstly, if this query ever gets modified even slightly in the future then the change will fail, and in a big query like this change is inevitably going to occur - also if another modification adds a variable before me, then my search will fail. So alternatively, how about searching for the smallest unique string possible, and just replacing that?
<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, don't search for:
// Version: 1.1 Beta 1; Modifications
Instead either use the xml attribute to add your entry to the end of the file, or otherwise search for:
?>
And add your entries before it. This will avoid your modification being locked to that version number - and mean you don't have to edit your modification very time a new version comes out!