Setting up my weblog. part 1 tagging
Posted on 06/05/2008 at 08:47 PM
So a major part of my weblog setup using the core version of Expression Engine was adding a way of tagging my posts. In the end I came up with an extension to add the admin functionality and a plugin to get various data into the frontend.
Creating the extension
Ok before we start you need to make sure that:
- Your MySql server has InnoDb table support
- You are running PHP5 ( seriously you shouldnt be using 4 anymore! )
- You have download Zend Framework and added into your include path
Next lets setup a skeletion structure for the extension. Create a file called ext.kp_tagging.php in the extensions dir.
The basic structure
- /**
- * Tagging extension for my weblog
- *
- * This file must be placed in the
- * /system/extensions/ folder in your ExpressionEngine installation.
- *
- * Important! Your mysql server must support innodb!!!!
- *
- * This extension uses Zend Framework make sure it is on the include path
- *
- * @package KPTagging
- * @version Version 0.0.1
- * @author Keith Pope
- * @copyright Copyright (c) 2007-2008 Keith Pope
- * @license New BSD License
- */
- /**
- * Manages extension activation, deactivation and upgrading, links class
- * methods to ExpressionEngine hooks and implements the administration interface.
- *
- * @package KPTagging
- * @version Version 0.0.1
- * @author Keith Pope <http://www.thepopeisdead.com>
- * @copyright Copyright (c) 2007-2008 Keith Pope
- * @license New BSD License
- */
- class kp_tagging
- {
- /**
- * Extension settings
- * @var array
- */
- /**
- * Extension name
- * @var string
- */
- public $name = 'KP Tagging';
- /**
- * Extension version
- * @var string
- */
- public $version = '0.0.1';
- /**
- * Extension description
- * @var string
- */
- public $description = 'Implements an interface to add tag data to ExpressionEngine weblog entries';
- /**
- * If $settings_exist = 'y' then a settings page will be shown in the ExpressionEngine admin
- * @var string
- */
- public $settings_exist = 'n';
- /**
- * Link to extension documentation
- * @var string
- */
- public $docs_url = 'http://www.thepopeisdead.com/tagging_extension';
- /**
- * Addon Name
- * @var string
- */
- public $addon_name = 'KP Tagging';
- /**
- * Log
- * @var array
- */
- /**
- * Debug
- * @var array
- */
- public $debug = FALSE;
- /**
- * Construct ( No setting atm )
- *
- * @param mixed $settings Optional settings array
- */
- public function __construct( $settings = '' )
- {
- $this->settings = $settings;
- }
- /**
- * Activate the extension in EE.
- *
- * @return bool Always TRUE
- */
- public function activate_extension()
- {}
- /**
- * Update the extension
- *
- * @param string $current
- */
- public function update_extension( $current = '' )
- {}
- /**
- * Disable the plugin
- */
- public function disable_extension()
- {}
- }
So here we have created the basic structure EE uses to activate, disable, and update extensions. The next step is to write the installer / activation method body.
Add the following into your activate_extension method:
- /**
- * Activate the extension in EE.
- *
- * @return bool Always TRUE
- */
- public function activate_extension()
- {
- $hooks = array( 'publish_form_new_tabs' => 'publish_form_new_tabs', 'publish_form_new_tabs_block' => 'publish_form_new_tabs_block', 'submit_new_entry_end' => 'submit_new_entry_end', 'show_full_control_panel_end' => 'show_full_control_panel_end' );
- foreach( $hooks as $hook => $method )
- {
- }
- /**
- * Now create the tagging tables, I am using the recommended schema
- * from mysql forge <a href="http://forge.mysql.com/wiki/TagSchema#Schema">http://forge.mysql.com/wiki/TagSchema#Schema</a>
- * I have found this to works well on other projects and its a pretty
- * interesting read hopefully someone will finish it one day.
- */
- //Change the exp_weblog_data to innodb
- $sql[] = 'ALTER TABLE exp_weblog_data ENGINE=InnoDB';
- //create the tag table
- $sql[] = 'CREATE TABLE IF NOT EXISTS exp_kp_tags (
- tag_id INT UNSIGNED NOT NULL AUTO_INCREMENT
- , tag_text VARCHAR(156) NOT NULL
- , PRIMARY KEY (tag_id)
- , UNIQUE INDEX (tag_text)
- ) ENGINE=InnoDB;';
- //create the tag links
- $sql[] = 'CREATE TABLE IF NOT EXISTS exp_kp_weblog2tag (
- entry_id INT UNSIGNED NOT NULL
- , tag_id INT UNSIGNED NOT NULL
- , PRIMARY KEY (entry_id, tag_id)
- , INDEX (tag_id)
- , FOREIGN KEY fk_Entry (entry_id) REFERENCES exp_weblog_data (entry_id) ON DELETE CASCADE
- , FOREIGN KEY fk_Tag (tag_id) REFERENCES exp_kp_tags (tag_id) ON DELETE CASCADE
- ) ENGINE=InnoDB';
- //run the sql
- foreach( $sql as $query )
- {
- $DB->query( $query );
- }
- return true;
- }
Ok so now what have we done, firstly we need to decide what hooks we are going to get our extension to extend from. This is done by inserting information about the plugin and hook into the exp_extensions table.
- 'publish_form_new_tabs' => 'publish_form_new_tabs',
- 'publish_form_new_tabs_block' => 'publish_form_new_tabs_block',
- 'submit_new_entry_end' => 'submit_new_entry_end',
- 'show_full_control_panel_end' => 'show_full_control_panel_end'
- );
- foreach( $hooks as $hook => $method )
- {
- 'method' => $method,
- 'hook' => $hook,
- 'settings' => '',
- 'priority' => 10,
- 'version' => $this->version,
- 'enabled' => "y" ) );
- }
Next we need to create a table for our tags and then a table to map our relationships. Also we alter the exp_weblog_data so its innodb, this is so we can have foreign key constrants.
Note about the tagging schema
I have used the tagging schema suggested on the MySQL Forge wiki, its well worth having a look at there are many tips on the performance of such a schema. http://forge.mysql.com/wiki/TagSchema#Schema
- //Change the exp_weblog_data to innodb
- $sql[] = 'ALTER TABLE exp_weblog_data ENGINE=InnoDB';
- //create the tag table
- $sql[] = 'CREATE TABLE IF NOT EXISTS exp_kp_tags (
- tag_id INT UNSIGNED NOT NULL AUTO_INCREMENT
- , tag_text VARCHAR(156) NOT NULL
- , PRIMARY KEY (tag_id)
- , UNIQUE INDEX (tag_text)
- ) ENGINE=InnoDB;';
- //create the tag links
- $sql[] = 'CREATE TABLE IF NOT EXISTS exp_kp_weblog2tag (
- entry_id INT UNSIGNED NOT NULL
- , tag_id INT UNSIGNED NOT NULL
- , PRIMARY KEY (entry_id, tag_id)
- , INDEX (tag_id)
- , FOREIGN KEY fk_Entry (entry_id) REFERENCES exp_weblog_data (entry_id) ON DELETE CASCADE
- , FOREIGN KEY fk_Tag (tag_id) REFERENCES exp_kp_tags (tag_id) ON DELETE CASCADE
- ) ENGINE=InnoDB';
- //run the sql
- foreach ($sql as $query)
- {
- $DB->query($query);
- }
Ok so thats the activation dealt with now we need to create the disable method body.
- /**
- * Disable the plugin ( we wont delete the tags do that yourself )
- */
- public function disable_extension()
- {
- }
This is very simple its just un-hooks the hooks in the ext_extensions table, I didn't delete the other tables I created as there may be valuable data there and its better to do it manually!
Obviously we dont have any upgrades to do so just make the update_extension method return true.
Ok thats it we have an extension that can be activated in the admin now, wait dont activate it just yet! Yeah we need to actually write the hooks else EE will try and call not existant methods!
The Hooks
Right to do what we want, we need to use the following hooks:
- publish_form_new_tabs
- publish_form_new_tabs_block
- submit_new_entry_end
- show_full_control_panel_end
For a full list of hooks available to you goto http://expressionengine.com/developers/extension_hooks/ this details all the aailable hooks and what they do.
First off we need to add in a tab on the entry add/edit page. So we use the publish_form_new_tabs hook.
- /**
- * Adds a new tab to the publish / edit form
- *
- * @param array $publish_tabs Array of existing tabs
- * @param int $weblog_id Current weblog id
- * @param int $entry_id Current entry id
- * @param array $hidden Hidden form fields
- * @return array Modified tab list
- */
- public function publish_form_new_tabs( $publish_tabs, $weblog_id, $entry_id, $hidden )
- {
- if( $EXT->last_call !== FALSE )
- {
- $publish_tabs = $EXT->last_call;
- }
- $publish_tabs['kpt'] = 'Tagging';
- return $publish_tabs;
- }
So here we are simply adding in a new tab with the text 'Tagging' into the $publish_tabs array. There are a couple of important things to note here.
It is very important to use the $EXT->last_call, if you just add the new tab and return the array no other extensions that use the same hook will work! This goes for all the hooks!
Also the key you you give your tab ( $publish_tabs['kpt'] ) is used to indentify your tab block, so remember it from later!
Ok now we have a tab but no tab block so the next thing to do is create the block.
Now here I chose to use Zend_View as I dont like creating html output in my php code! If you want to use the standard EE way look at the $DSP global var which has a load of methods for creating html. I personally dont like this method of creating html as it ties display with logic and is also very hard to read! I have enough problems creating DOM elements this way in Javascript.
So first add this into the header of the file:
- /* Zend_View */
- require_once 'Zend/Loader.php';
- Zend_Loader::loadClass( 'Zend_View' );
This will load Zend_View, now we can create the tab block.
- /**
- * Render the tagging form block, uses Zend_View as
- * I was too lazy to write loads of php to produce the html :)
- *
- * @param int $weblog_id
- * @return string
- */
- public function publish_form_new_tabs_block( $weblog_id )
- {
- if( $EXT->last_call !== FALSE )
- {
- $ret = $EXT->last_call;
- }
- $view = new Zend_View();
- $view->Doctype( 'XHTML1_STRICT' );
- $view->tagText = '';
- //editing?
- if( $entry_id = $IN->GBL( 'entry_id', 'GET' ) )
- {
- // get the tags
- $query = $DB->query( 'SELECT tag_text
- FROM exp_kp_tags, exp_kp_weblog2tag
- where exp_kp_tags.tag_id = exp_kp_weblog2tag.tag_id
- AND exp_kp_weblog2tag.entry_id = "' . $entry_id . '"
- GROUP BY exp_kp_tags.tag_id' );
- // if records
- if( $query->num_rows > 0 )
- {
- foreach( $query->result as $row )
- {
- $tags[] = $row['tag_text'];
- }
- }
- }
- $view->setScriptPath( $path . '/kp_tagging' );
- $view->prefs = $PREFS;
- $ret = $view->render( 'tagging_block_view.phtml' );
- return $ret;
- }
And the view file can be placed in extensions/kp_tagging/tagging_block_view.phtml
- <div id="blockkpt">
- <div class="publishTabWrapper">
- <div class="publishBox">
- <div class="publishInnerPad">
- <table cellspacing="0" cellpadding="0" border="0" style="width: 99%;">
- <tr>
- <td>
- <h5>Enter Tags</h5>
- <p>Seperate tags with commas</p>
- </td>
- <td style="width: 350px;">
- </td>
- </tr>
- </table>
- </div>
- </div>
- </div>
- </div>
This will give us this:
Also in this method we check if we are editing a record by checking the GET for entry_id, if we are editing we retrieve the tags for this entry.
Now we need to handle submit_new_entry_end hook, this hook is called when you add/edit an entry.
- /**
- * Add the tags into the db
- *
- * @param int $entry_id
- * @param array $data
- * @param string $ping_message
- */
- public function submit_new_entry_end( $entry_id, $data, $ping_message )
- {
- {
- $tags = $_POST['kp_tags'];
- $count = 0;
- foreach( $tags as $tag )
- {
- {
- continue;
- }
- $count++;
- //first add any new tags!
- $query = $DB->query( "SELECT * FROM exp_kp_tags WHERE tag_text='" . $DB->escape_str( $tag ) . "' LIMIT 1" );
- if ($query->num_rows == 0)
- {
- $DB->query( $insert );
- $addedTags[] = $DB->insert_id;
- continue;
- }
- $existingTag = $query->row;
- $addedTags[] = $existingTag['tag_id'];
- }
- if( $count != 0 )
- {
- //unlink all (not most efficient but we have no edit hook :( )
- $sql = "DELETE FROM exp_kp_weblog2tag WHERE entry_id = " . $entry_id;
- $DB->query( $sql );
- //now link the tags to the entry
- foreach( $addedTags as $tagID )
- {
- $insert = $DB->insert_string( 'exp_kp_weblog2tag', $row );
- $DB->query( $insert );
- }
- }
- }
- }
Here for some reason there isnt a edit hook, so I just delete all the tags and re-add them for each submission. I will tidy this up as time goes by but it works for now...
The final hook is the show_full_control_panel_end this gives you the full page of the control panel so we have to do a replace to insert our JS and CSS.
- /**
- * Process the final control panel output
- *
- * @param string $out
- * @return string
- */
- public function show_full_control_panel_end( $out )
- {
- if( $EXT->last_call !== FALSE )
- {
- $out = $EXT->last_call;
- }
- $r = '<script type="text/javascript" src="/scripts/mootools_full.js"></script>' . "\n";
- $r .= '<script type="text/javascript" src="/scripts/kptagging.js"></script>' . "\n";
- $r .= "<style type='text/css' media='screen'>
- #blockkpt{display:none;}
- #blockkpt input,
- #blockkpt textarea{display:block; width:99%}
- #blockkpt .highlight{margin:10px;}
- #blockkpt .multi{overflow:auto;}
- #blockkpt .multi div{float:left; width:33%;}
- #blockkpt .multi input,
- #blockkpt .multi select{display:block; width:80%;}
- #blockkpt .character-count{margin:9px 0}
- #blockkpt .toomuch{color:#ff0000};
- #blockkpt .keywords input{display:inline; margin-right:5px; width:80%}
- #blockkpt .hidden{display:none;}
- </style>";
- // add the script string before the closing head tag
- // make sure we don't add it again
- return $out;
- }
The Plugin
Once the extension is created you should be able to enable it in the control panel! But there is one more part to the extension, thats a plugin to pull the data into the frontend.
Plugins are quite simple in EE however I didn't want to reproduce the weblog functionality in my plugin so it got a little complex with me opting to extend the weblog module. This is not the best way to do this but again I wanted something up quickly.
Heres the plugin code ( pi.kp_tag.php )
- <?php
- /**
- * Plugin File for kp tagging
- *
- * This file must be placed in the
- * /system/plugins/ folder in your ExpressionEngine installation.
- *
- * @package KPTagging
- * @version Version 0.0.1
- * @author Keith Pope <http://www.thepopeisdead.com>
- * @copyright Copyright (c) 2007-2008 Keith Pope
- * @license New BSD License
- */
- /**
- * Plugin information used by ExpressionEngine
- * @global array $plugin_info
- */
- 'pi_name' => 'KP Tagging',
- 'pi_version' => '0.0.1',
- 'pi_author' => 'Keith Pope',
- 'pi_author_url' => 'http://www.thepopeisdead.com/',
- 'pi_description' => 'Get a tag cloud',
- 'pi_usage' => 'Its easy...'
- );
- require_once $p . '/mod.weblog' . EXT;
- /**
- * Tag cloud....
- *
- * @package KPTagging
- * @version Version 0.0.1
- * @author Keith Pope <http://www.thepopeisdead.com>
- * @copyright Copyright (c) 2007-2008 Keith Pope
- * @license New BSD License
- */
- class Kp_tag extends Weblog
- {
- /**
- * Returned string
- * @var array
- */
- public $return_data = "";
- /**
- * Plugin version
- * @var array
- */
- public $version = "0.0.1";
- /**
- * Store for when we are getting tagged
- * items for one tag.
- *
- * @var string
- */
- private $kp_tag;
- /**
- * Not used...
- *
- * @return Kp_tag
- */
- public function Kp_tag()
- {}
- /**
- * Get the tag cloud
- *
- * @return string
- */
- public function cloud()
- {
- $output = '';
- $sql = 'SELECT tag_text
- , COUNT(*) as num_items
- FROM exp_kp_weblog2tag i2t
- INNER JOIN exp_kp_tags t
- ON i2t.tag_id = t.tag_id
- GROUP BY tag_text';
- $query = $DB->query( $sql );
- if( $query->num_rows > 0 )
- {
- foreach( $query->result as $row )
- {
- $tags[] = $row['tag_text'];
- $totals[] = $row['num_items'];
- }
- //do some calculations
- $max_size = 30; // max font size in pixels
- $min_size = 11; // min font size in pixels
- // largest and smallest array values
- // find the range of values
- $spread = $max_qty - $min_qty;
- if( $spread == 0 )
- {
- $spread = 1;
- }
- // set the font-size increment
- $step = ( $max_size - $min_size ) / ( $spread );
- $output = '';
- // loop through the tag array
- foreach( $tags as $key => $value )
- {
- $output .= '<a href="/main/tagged/' .
- '" style="font-size: ' .
- $size .
- 'px" title="' .
- $totals[$key] .
- ' things tagged with ' .
- $value . '">' .
- }
- }
- return $output;
- }
- /**
- * Get entires tagged with something...
- *
- * Expects tag in $_GET
- *
- * @see parse_weblog_entries()
- * @return string
- */
- public function GetTaggedEntries()
- {
- $this->limit = 1;
- parent::Weblog();
- parent::entries();
- return $this->return_data;
- }
- /**
- * Override the parsing in parent::entries so
- * we can augment the query and use the same tags
- * we normally do in all the weblog entries!
- *
- * This does run 2 queries but this was the only way
- * I could work around...
- */
- public function parse_weblog_entries()
- {
- $this->p_limit = 10;
- $this->page_next = '';
- $this->page_previous = '';
- $this->pagination_links = '';
- $this->total_pages = 1;
- $sql = "select wt.entry_id FROM exp_kp_tags t
- JOIN exp_kp_weblog2tag wt ON t.tag_id = wt.tag_id
- where t.tag_text = '{$DB->escape_str( $this->kp_tag )}'";
- $q = $DB->query( $sql );
- if( $q->num_rows == 0 )
- {
- $this->return_data = 'No items tagged ' . $this->kp_tag;
- return;
- }
- foreach( $q->result as $row )
- {
- $ids[] = $row['entry_id'];
- }
- $total = $q->num_rows;
- $this->create_pagination($total);
- $page = $this->p_page == null ? 0 : $this->p_page;
- $this->sql .= " LIMIT ". $page . ', ' . $this->p_limit;
- $this->query = $DB->query( $this->sql );
- parent::parse_weblog_entries();
- }
- /**
- * Plugin usage documentation
- *
- * @return string Plugin usage instructions
- */
- {
- return "For usage visit: <a href="http://www.thepopeisdead.com";">http://www.thepopeisdead.com";</a>
- }
- }
The plugin does two things it gets a tag cloud and get entries related to a tag. I still think there must be a better way of doing this in EE hopefully I will figure it out as I go along! At the moment I am rewriting the query in parse_weblog_entries which works but its not very pretty. One good thing is that our plugin has all the same tag support as the weblog module now :)
Thats about it for the tagging extension, the only bit I have left out is the Ajax auto-suggest. But I want to do some tutorials on Mootools soon anyway. For now you can download the source below:
Download KpTagging Extension 0.0.1
There are still loads of improvements I want to add to this but it got me up and running quickly, I will update with improvements when I get time. If you need any help leave a comment or email me...