Converting Genoa plugins to Siena

This howto contains general rules and an example of how to convert 0.6.x plugins to 0.9.x

#1. General notes

This section contains general notes about what has changed in Cotonti plugins API in Siena and what actions should be performed to convert a Genoa plugin to 0.9.x.

#1.1. Folder structure and file names

Since 0.9.0 Cotonti applies strict rules on naming files and directories. All plugin files should be prefixed with plugin code so that it is easy to distinguish files by name in multi-tab development environments. For more information on file names in Siena, please see Directory structure for modules and plugins.

Also notice that plugin install/update handlers have been moved from plugin_code.setup.php to separate files in setup subfolder.

#1.2. Headers of PHP parts

Format of header comments has been simplified. BEGIN_SED_EXTPLUGIN is now BEGIN_COT_EXT, END_SED_EXTPLUGIN is now END_COT_EXT. Most properties are optional. The only required property is Hooks (you can also define multiple hooks per part, separated with comma).

In setup.php files there are 4 new properties for dependencies control in Siena:

  • Requires_modules - comma separated list of modules which must be installed before using the plugin.
  • Recommends_modules - comma separated list of modules which are not required but can benefit something from this plugin.
  • Requires_plugins - same as for modules, but lists plugin codes.
  • Recommends_plugins - same as for modules, but lists plugin codes.

#1.3. Path functions

Use cot_incfile(), cot_langfile() and cot_tplfile() functions to get paths to PHP, LANG and TPL file accordingly. Notice that sed_skinfile() function has been replaced with cot_tplfile() and arguments usage is different in Siena. For a quick search and replace, theres a regex search/replace statement in the table below.

#1.4. Prefix change: sed_ => cot_

In most functions, constants and variables sed_ prefix has been changed with cot_. Also signatures of some functions may have changed and some functions may be moved or removed. If you want some information about some particular function, search the Code Reference. Some basic rules of semi-automatic conversion with "Find & Replace in Files" is given below.

#1.5. New database layer

Since 0.9.0 Cotonti uses PDO database layer which may be familiar to many PHP coders but may be is not familiar to a Cotonti plugin maker. This change probably requires the most manual work, but still most of database calls can be converted automatically (see subsection below). The main visible difference is that instead of calling sed_sql_* functions, you call similar methods of $db object which represents a database  connection. For example, $db->query() is the most popular call. Also fetching methods are called on result set objects, e.g. fetching a database row now looks like $row = $res->fetch().

It is strongly recommended to use use $db->insert(), $db->update() and $db->delete() for appropriate data manipulation operations. For more information about $db object methods see CotDB class reference and PDOConnection reference. For information about rowset operations, see PDOStatement reference.

There is an important rule about fetch loops in PDO. If database queries may be executed inside of the database fetch loop, then use foreach + $db->fetchAll() sequence instead of while + $db->fetch(). Example:

$res = $db->query("SELECT * FROM $db_foo");
while ($row = $res->fetch())
	// It is OK unless other $db calls are placed here
	// Also avoid hooks inside of such loops

$res = $db->query("SELECT * FROM $db_foo");
foreach ($res->fetchAll() as $row)
	// You can safely make other $db calls
	// and put hooks here
	$res2 = $db->query("SELECT * FROM $db_bar");

Another useful PDO feature is query placeholders. They make queries a bit cleaner and protect from SQL injections. Placeholders are supported in query(), delete() and update() conditions. Placeholders are optional but recommended for values received from users, e.g.:

$text = cot_import('text', 'P', 'TXT');
$num = cot_import('num', 'P', 'INT');

$res = $db->query("SELECT * FROM $db_foobar WHERE foo_text = ? AND foo_num = ?", array($text, $num));

As $db is a global variable, you need to add $db to the globals list of any function which queries the database. Example:

function my_func($some_arg)
	global $db, $db_my_table, $cfg;
	// Now we can perform queries

#1.6. Pagination

If your plugin uses sed_pagination() then you need a bit more changes. First of all, sed_pagination() has been replaced with cot_pagenav() function which has a little bit different arguments and it returns an array of pagination controls. So if you had code similar to this:

$pagination = sed_pagination(sed_url('plug', 'e=donations&m=admin'), $d, $totallines, $cfg['maxrowsperpage']);
list($pageprev, $pagenext) = sed_pagination_pn(sed_url('plug', 'e=donations&m=admin'), $d, $totallines, $cfg['maxrowsperpage'], TRUE);

Then it can now be replaced with the following:

$pagenav = cot_pagenav('plug', 'e=donations&m=admin', $d, $totallines, $cfg['maxrowsperpage']);
$pagination = $pagenav['main'];
$pageprev = $pagenav['prev'];
$pagenext = $pagenav['next'];

The last 3 lines are not necessary if you access pagination components directly with array indexes.

Cotonti Siena supports 2 pagination modes: with page numbers (1, 2, 3,..) and database offsets (0, 15, 30,..) in the URLs. It requires that you need to change the way the offset variable is imported. So if you had something like:

$d = sed_import('d', 'G', 'INT');
$d = (empty($d)) ? 0 : $d;

It should now become:

list($pn, $d, $d_url) = cot_import_pagenav('d', $cfg['maxrowsperpage']);

Then you can use $d as offset for SQL LIMIT, $pn as actual page number and $d_url in the URLs because it can be either $d or $pn depending on your pagination mode choice. Example URL usage:

$url = cot_url('plug', 'e=donations&m=admin&d=' . $d_url);

#1.7. Find & Replace in Files tips

If your IDE/Editor has a "Find & Replace in Files" tool (for example, in NetBeans IDE you select a folder in Projects or Files tab and the click Edit -> Replace in Projecs menu or Ctrl+Shift+H) and supports regular expressions in this tool, you can use this cookbook to convert database and function calls automatically.

Apply the rules in the same order as given, the order is important. Rules with RegExp flag require switching "Regular expression" mode on in the Search & Replace tool, rules without such flag should be ran in normal mode.

Pattern Replacement RegExp?
sed_skinfile\((.*?), true\) cot_tplfile($1, 'plug') Yes
sed_cache_clear( $cache && $cache->db->remove( No
sed_cache_clearall() $cache && $cache->db->clear() No
sed_cache_get( $cache && $cache->db->get( No
sed_cache_store\((.+),(.+?)\) \$cache && \$cache->db->store($1, COT_DEFAULT_REALM,$2) Yes
sed_sql_result( Manual, see below -
sed_sql_query( $db->query( No
sed_sql_fetcharray\((\$\w+)\) $1->fetch() Yes
sed_sql_fetchassoc\((\$\w+)\) $1->fetch() Yes
sed_sql_fetchrow\((\$\w+)\) $1->fetch(PDO::FETCH_NUM) Yes
sed_sql_affectedrows() $db->affectedRows No
sed_sql_freeresult\((\$\w+)\) $1->closeCursor() Yes
sed_sql_insertid() $db->lastInsertId() No
sed_sql_numrows\((\$\w+)\) $1->rowCount() Yes
sed_sql_prep( $db->prep( No
sed_ cot_ No

All searches are case-sensitive.

sed_sql_result() syntax is very different, so you should convert it manually before applying other replacements. For example, if you find something like:

sed_sql_result(sed_sql_query("SELECT COUNT(*) FROM $db_pages
	WHERE page_alias = '$alias' AND page_id != $id"), 0, 0)

the replacement will be a call of fetchColumn() method for that code:

$db->query("SELECT COUNT(*) FROM $db_pages
	WHERE page_alias = '$alias' AND page_id != $id")->fetchColumn()

Here I have replaced sed_sql_query() with $db->query() call first and then called fetchColumn() method on the query result object.

#2. Conversion example: autoalias2

In this section I give a step by step example by converting autoalias2 plugin from Genoa to Siena. You can download the original Genoa plugin here and then download the result plugin for Siena here.

#2.1. Step 1: file names

First we need to make sure that all our files are prefixed with plugin code correctly. I skip this step because autoalias2 plugin already has valid file names: autoalias2.functions.php,, autoalias2.setup.php, autoalias2.en.lang.php, etc.

#2.2. Step 2: file headers

Next thing is header comment modification. Let's start by modifying the autoalias2.setup.php file. Modified lines are highlighted below:

/* ====================
Name=AutoAlias 2
Description=Creates page alias from title if a user leaves it empty
Copyright=All rights reserved (c) 2010-2011, Vladimir Sibirov.
Notes=BSD License

translit=01:radio::0:Transliterate non-latinic characters if possible
prepend_id=02:radio::0:Prepend page ID to alias
on_duplicate=03:select:ID,Random:ID:Number appended on duplicate alias (if prepend ID is off)
sep=04:select:-,_,.:-:Word separator
lowercase=05:radio::0:Cast to lower case
==================== */

Lines 6 and 7 are just version update. Version information is important for automatic updates in Siena. Line 15 tells the installer that page module is required for this plugin to run.

Then let's modify other PHP part headers. For example, in autoalias2.admin.php we replace

/* ====================
==================== */

with a simplified equivalent:

/* ====================
==================== */

We could keep the Order property too, but 10 is the default value for it, so there is no need to keep it there. Another property which is often kept is Tags, but in this plugin it is empty, so we omit it. We modify header comments for other files in a similar way.

Another replacement on this step is changing prefix of SED_CODE and similar constants, so that

defined('SED_CODE') or die('Wrong URL');


defined('COT_CODE') or die('Wrong URL');

You can do it with simple case-sensitive Find & Replace in File: SED_ => COT_. Once again: for constants it is case-sensitive.

#2.3. Step 3: include files

File paths are no longer hardcoded, there are special functions for path hinting. So I replace where necessary

require_once $cfg['plugins_dir'] . '/autoalias2/inc/autoalias2.functions.php';


require_once cot_incfile('autoalias2', 'plug');

Arguments are unified between cot_incfile(), cot_langfile() and cot_tplfile() functions. For example, if I wanted to create a XTemplate object for a 'foobar.edit.tpl' template of a 'foobar' plugin, I would do it like this:

$t = new XTemplate(cot_tplfile('foobar.edit', 'plug'));

#2.4. Step 4: prefixes and database functions

The next thing we need to do is applying "Find & Replace in Files tips" from the previous chapter. I have performed it on all files, but for autoalias2.admin.php I will highlight the lines which have been changed:

$plugin_title = 'AutoAlias';

if ($a == 'create')
	$count = 0;
	$res = $db->query("SELECT page_id, page_title FROM $db_pages WHERE page_alias = ''");
	foreach ($res->fetchAll() as $row)
		autoalias2_update($row['page_title'], $row['page_id']);
	$plugin_body .= <<<HTM
<div class="error">
	{$L['aliases_written']}: $count

$create_url = cot_url('admin', 'm=other&p=autoalias2&a=create');
$plugin_body .= <<<HTM
<a href="$create_url">{$L['create_aliases']}</a>

It is important to noitce that in admin URLs m=tools is now replaced with m=other.

Another interesting tweak in autoalias2.functions.php is replacement of regular query() call with a specialized update() method:

$db->query("UPDATE $db_pages SET page_alias = '$alias' WHERE page_id = $id");


$db->update($db_pages, array('page_alias' => $alias), "page_id = $id");

And we also add $db to the globals list in function autoalias2_update():

global $cfg, $db, $db_pages;

#2.5. Step 5: tweaks related to changes in modules

Some modifications requried because underlying code of page module has been changed. In at line 20 $newpagealias => $rpage['page_alias'], at line 23 $newpagetitle => $rpage['page_title'] and in at line 20 $rpagealias => $rpage['page_alias'] at line 23 $rpagetitle => $rpage['page_title']:

if (empty($rpage['page_alias']))
	require_once cot_incfile('autoalias2', 'plug');
	autoalias2_update($rpage['page_title'], $id);

But wait, the code has become identical for both files! This means that we can replace them both with just 1 file using multihook feature:

/* ====================
==================== */

 * Creates alias when adding or updating a page
 * @package autoalias2
 * @version 2.1.0
 * @author Trustmaster
 * @copyright (c) 2010-2011 Vladimir Sibirov
 * @license BSD

defined('COT_CODE') or die('Wrong URL');

if (empty($rpage['page_alias']))
	require_once cot_incfile('autoalias2', 'plug');
	autoalias2_update($rpage['page_title'], $id);

#2.6. Done

Our plugin is ready!

1. Leqacy  2011-04-22 20:56

We should use the Easy Way Do not have you getting?

2. Dyllon  2011-04-29 13:50

realistically thier is no "easy way" to accomplish this due to 0.9.0 being such a large overhaul.

Only registered users can post new comments