Documentation

Light-weight, fast, simple and powerful!
 
Translations of this page?:

Writing a plugin (part 1)

In this tutorial I’ll show you how to develop a simple Menu plugin for WolfCMS. By default, menus in Wolf are simply a list of active Pages. But what if you wanted to add a menu item linking to an external website? The plugin we’ll be building will have that functionality.

In part 1 we will:

  1. discuss the WolfCMS plugin structure
  2. setup the basic structure of the plugin
  3. create the database tables
  4. code the plugin enable, disable & uninstall scripts
  5. code the plugin models required to fetch data from the database tables.
  6. create the sidebar and menu_index view files so we can view our plugins default page.

Plugin structure

Plugins in Wolf have a logical file structure. A typical plugin will contain these files.

  • views/ – The views directory contains the views (or the ‘html layouts’) of your plugin’s pages which will be displayed in the backend (forms, documentation, settings, etc).
  • disable.php – Executed when you disable the plugin.
  • enable.php – Executed when you enable the plugin.
  • index.php – Contains information about the plugin which it provides to the CMS, you can also load JavaScript files and models in the plugin index file.
  • plugin_id.css (optional) If a stylesheet with your plugin name as the filename is found it is automatically loaded.
  • PluginIdController.php – Your plugin controller contains its actions, what it does, how it does it.
  • uninstall.php – Executed when you uninstall the plugin.

Your plugin id is your plugin’s name, it is also the name of the folder your plugins files will reside in.

You can also include models (classes you can use to interact with the plugin’s database tables). In Wolf, each model represents a single table record, with static methods for querying the table). Our plugin will have two models, one called ‘Menu’ and the other ‘MenuItem’ and we’ll store them in a folder called models.

Let’s get started! WolfCMS happens to come with a very useful skeleton plugin (an empty plugin with the basic structure in place), so rather than write one from scratch, let’s copy that and rename it.

- Open up /wolfcms/wolf/plugins/ - Create a copy of the skeleton plugin and rename the copy to menu.

Lets start with the plugin’s main index file.

Index.php

As mentioned earlier, the index.php file tells WolfCMS about your plugin. Its in this file you provide all the neccessary plugin information and load any models & javascript files in your plugin.

This is our plugin’s index.php file, copy and paste it in.

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
// Provide the neccessary plugin information for Wolf.
Plugin::setInfos(array(
	'id'		  => 'menu',
	'title'	   => __('Menu'),
	'description' => __('A simple menu system for WolfCMS'),
	'version'	 => '1.0.0',
	'author'	  => 'Your name here',
	'website'	 => 'http://www.example.com',
	'require_wolf_version' => '0.5.5'
));
 
/**
 * Load the plugin controller.
 *
 * addController explained:
 * 'menu' => The plugin ID.
 * 'Menu' => The plugin tab label.
 * 'administrator' => permission name or group name - restricts who can access the controller.
 * 'true' => If set to true, a plugin tab is displayed in the backend navigation menu (default = false).
 */
Plugin::addController('menu', 'Menu', 'administrator', true);
 
// Load the plugin models.
AutoLoader::addFolder(PLUGINS_ROOT.DS.'menu'.DS.'models');

The Database Tables

The plugin will store data on two things, menus and menu items. Below is the SQL code for creating those tables which we will use in our plugin’s enable script.

Table: menu

CREATE TABLE IF NOT EXISTS `menu` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`is_active` enum('T','F') NOT NULL DEFAULT 'T',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Table: menu_item

CREATE TABLE IF NOT EXISTS `menu_item` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`menu_id` int(11) NOT NULL,
`page_id` int(11) NOT NULL,
`link_type` enum('I','E') NOT NULL DEFAULT 'I',
`label` varchar(100) NOT NULL,
`url_dest` varchar(200) NOT NULL,
`link_title_attr` varchar(100) NOT NULL,
`css_class` varchar(50) NOT NULL,
`css_id` varchar(50) NOT NULL,
`position` int(11) NOT NULL DEFAULT '1',
`is_active` enum('T','F') NOT NULL DEFAULT 'T',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

Enable.php

When you install a plugin in Wolf, you simply copy the plugin’s folder into the WolfCMS plugin folder and enable it in the CMS backend, the plugin’s enable.php script is then executed.

Below is the code for our plugin enable script, I’ve provided comments to explain what’s happening.

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
// Grab the connection from Wolf.
$conn = Record::getConnection();
 
// Create the menu table (if non existant).
$conn->exec("CREATE TABLE IF NOT EXISTS `menu` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  `is_active` enum('T','F') NOT NULL DEFAULT 'T',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;");
 
// Create the menu item table (if non existant).
$conn->exec("CREATE TABLE IF NOT EXISTS `menu_item` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `menu_id` int(11) NOT NULL,
  `page_id` int(11) NOT NULL,
  `link_type` enum('I','E') NOT NULL DEFAULT 'I',
  `label` varchar(100) NOT NULL,
  `url_dest` varchar(200) NOT NULL,
  `link_title_attr` varchar(100) NOT NULL,
  `css_class` varchar(50) NOT NULL,
  `css_id` varchar(50) NOT NULL,
  `position` int(11) NOT NULL DEFAULT '1',
  `is_active` enum('T','F') NOT NULL DEFAULT 'T',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;");
 
// Insert some test records to play with later on.
$conn->exec("INSERT INTO `menu` (`id`, `name`, `is_active`) VALUES (1, 'header', 'T'), (2, 'primary', 'T');");
$conn->exec("INSERT INTO `menu_item` (`id`, `menu_id`, `page_id`, `link_type`, `label`, `url_dest`, `link_title_attr`, `css_class`, `css_id`, `position`, `is_active`) VALUES
	(1, 1, 1, 'I', 'about us', '/about-us', '', '', '', 1, 'T'),
	(2, 1, 2, 'I', 'contact us', '/contact-us', '', '', '', 2, 'T'),
	(3, 2, 0, 'I', 'home', '/', 'go back to the home page', '', '', 1, 'T');");
 
exit();

Disable.php

This script is executed when you disable the plugin in the backend. There’s nothing to be disabled in this plugin so you can just leave this file as it is. An example of why you might use this file is, you may have a plugin that generates site visitor statistics and creates a div in your home page layout to display this data. In this instance you could use the disable script to remove that div.

Uninstall.php

This script is executed when you uninstall the plugin. You can use this file to remove your plugin tables, any settings you’ve inserted and any physical files outside the plugin folder.

We’ll be using it to uninstall our plugin tables.

Note: To uninstall a plugin, you have to disable it first. Wolf doesn’t automatically disable a plugin when you try to uninstall it.

Copy and paste the following into uninstall.php:

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
// Grab the connection from Wolf.
$conn = Record::getConnection();
 
// Uninstall tables.
$conn->exec("DROP TABLE menu");
$conn->exec("DROP TABLE menu_item");
 
exit();

The Models

Your plugin models are what you’ll be using to store and retrieve data from the tables. They are classes which represent single table records as objects but also contain static methods to query the table. As I mentioned earlier, our plugin have two models – one for each of the tables. One for menus, and one for menu items. They will both be almost identical to begin with but we’ll add more methods to each of them as we need them later on. Right now they both have two methods, find() and findById() – all of our read queries will use the find method to fetch data from the tables. Create a folder in the plugin directory called models, and add the following two files.

Model: Menu (models/Menu.php)

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
class Menu extends Record
{
	const TABLE_NAME = 'menu';
 
	// Table fields.
	public $name;
	public $is_active;
 
	/**
	 * Perform a detailed query on the table.
	 */
	public static function find($args=array())
	{
		// Process & tidy up data from $args.
		$where = isset($args['where']) ? trim($args['where']) : false;
		$order = isset($args['order']) ? trim($args['order']) : false;
		$limit = isset($args['limit']) ? (int) $args['limit'] : false;
		$offset = isset($args['offset']) ? (int) $args['offset'] : false;
 
		// Prepare $arg data for SQL string.
		$bits = array(
			'where' => $where !== false ? "WHERE {$where}" : '',
			'order' => $order !== false ? "ORDER BY {$order}" : '',
			'limit' => $limit !== false ? "LIMIT {$limit}" : '',
			'offset' => $offset !== false ? "OFFSET {$offset}" : '',
		);
 
		// Construct the SQL string.
		$sql = "
			SELECT *
			FROM ".self::TABLE_NAME."
			{$bits['where']} {$bits['order']}
			{$bits['limit']} {$bits['offset']}
		";
 
		// Execute the query.
		$stmt = self::$__CONN__->prepare($sql);
		$stmt->execute();
 
		// Return a single object if limit is set to 1
		// or an array of objects if not.
		if ($limit == 1) {
			return $stmt->fetchObject('Menu');
		} else {
			$objects = array();
			while ($obj = $stmt->fetchObject('Menu')) {
				$objects[] = $obj;
			}
 
			return $objects;
		}
	}
 
	/**
	 * Fetches a record from the database by ID.
	 * Returns the object if found, or false if an invalid ID is passed or a record with that ID is not found.
	 */
	public static function findById($id)
	{
		if (!is_numeric($id)) { return false; }
 
		$menu = self::find(array(
			'where' => "id={$id}",
			'limit' => 1
		));
 
		if (!isset($menu) || !is_object($menu)) {
			return false;
		}
 
		return $menu;
	}
 
	/**
	 * Find and retrieve the first menu record.
	 */
	public static function findFirst()
	{
		$menu = self::find(array(
			'order' => 'id ASC',
			'limit' => 1
		));
 
		if (!isset($menu) || !is_object($menu)) {
			return false;
		}
 
		return $menu;
	}
 
	/**
	 * Find and retrieve the last menu record.
	 */
	public static function findLast()
	{
		$menu = self::find(array(
			'order' => 'id DESC',
			'limit' => 1
		));
 
		if (!isset($menu) || !is_object($menu)) {
			return false;
		}
 
		return $menu;
	}
}

Model: MenuItem (models/MenuItem.php)

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
class MenuItem extends Record
{
	const TABLE_NAME = 'menu_item';
 
	// Table fields.
	public $menu_id;
	public $page_id;
	public $link_type;
	public $label;
	public $url_dest;
	public $link_title_attr;
	public $css_class;
	public $css_id;
	public $position;
	public $is_active;
 
	/**
	 * Perform a detailed query on the table.
	 */
	public static function find($args=array())
	{
		// Process & tidy up data from $args.
		$where = isset($args['where']) ? trim($args['where']) : false;
		$order = isset($args['order']) ? trim($args['order']) : false;
		$limit = isset($args['limit']) ? (int) $args['limit'] : false;
		$offset = isset($args['offset']) ? (int) $args['offset'] : false;
 
		// By default, order by the position field.
		if (!$order) { $order = 'position ASC'; }
 
		// Prepare $arg data for SQL string.
		$bits = array(
			'where' => $where !== false ? "WHERE {$where}" : '',
			'order' => $order !== false ? "ORDER BY {$order}" : '',
			'limit' => $limit !== false ? "LIMIT {$limit}" : '',
			'offset' => $offset !== false ? "OFFSET {$offset}" : '',
		);
 
		// Construct the SQL string.
		$sql = "
			SELECT *
			FROM ".self::TABLE_NAME."
			{$bits['where']} {$bits['order']}
			{$bits['limit']} {$bits['offset']}
		";
 
		// Execute the query.
		$stmt = self::$__CONN__->prepare($sql);
		$stmt->execute();
 
		// Return a single object if limit is set to 1
		// or an array of objects if not.
		if ($limit == 1) {
			return $stmt->fetchObject('MenuItem');
		} else {
			$objects = array();
			while ($obj = $stmt->fetchObject('MenuItem')) {
				$objects[] = $obj;
			}
 
			return $objects;
		}
	}
 
	/**
	 * Fetches a record from the database by ID.
	 * Returns the object if found, or false if an invalid ID is passed or a record with that ID is not found.
	 */
	public static function findById($id)
	{
		if (!is_numeric($id)) { return false; }
 
		$item = self::find(array(
			'where' => "id={$id}",
			'limit' => 1
		));
 
		if (!isset($item) || !is_object($item)) {
			return false;
		}
 
		return $item;
	}
 
	/**
	 * Find and retrieve the first menu item record.
	 */
	public static function findFirst()
	{
		$item = self::find(array(
			'order' => 'id ASC',
			'limit' => 1
		));
 
		if (!isset($item) || !is_object($item)) {
			return false;
		}
 
		return $item;
	}
 
	/**
	 * Find and retrieve the last menu item record.
	 */
	public static function findLast()
	{
		$item = self::find(array(
			'order' => 'id DESC',
			'limit' => 1
		));
 
		if (!isset($item) || !is_object($item)) {
			return false;
		}
 
		return $item;
	}
 
	/**
	 * Find all items within a menu.
	 */
	public static function findByMenuId($menu_id)
	{
		if (!is_numeric($menu_id)) { return false; }
 
		$items = self::find(array(
			'where' => "menu_id={$menu_id}",
		));
 
		if (empty($items)) { return false; }
 
		return $items;
	}
}

The Controller

Your plugin controller IS your plugin. It is a class, with each method performing a specific action of your plugin in the backend.

For example: Lets say your plugin had a method called ‘new_post’ which displayed a form. When you visit http://www.example.com/admin/plugin/my_plugin/new_post Wolf creates an object from the ‘my_plugin’ controller class and attempts to call the method ‘new_post’.

Note: Only public methods can be accessed via the URL, its best to use public methods to display pages and use private methods to process forms.

- Rename SkeletonController.php to MenuController.php. - Open the controller in your text editor of choice and delete all the comments. - Change ‘class SkeletonController extends’ to ‘class MenuController extends’. - Delete the documentation and settings methods and remove $this→documentation() from the index method. - Replace all other occurances of ‘skeleton’ with ‘menu’ and you should be left with this:

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
class MenuController extends PluginController {
 
	public function __construct() {
		$this->setLayout('backend');
		$this->assignToLayout('sidebar', new View('../../plugins/menu/views/sidebar'));
	}
 
	/*
	 * Display the index form with either the first menu
	 * or the menu who's ID is passed in the methods only argument
	 */
	public function index($menu_id=false)
	{
		// If post data found, call the update processor method.
		if (isset($_POST['item_position'])) {
			$this->update();
		}
		else {
			// $view_data = variables to be passed into the view file.
			$view_data = array();
 
			// If no menu ID is passed, load the first menu.
			if (!$menu_id) {
				$menu = Menu::findFirst();
			}
			// If a menu ID has been passed, find a menu with that ID.
			else {
				$menu = Menu::findById($menu_id);
			}
 
			// If a menu has been found, load items.
			if (isset($menu) && is_object($menu))
			{
				$view_data['menu'] = $menu;
 
				// Lookup items by menu id.
				$items = MenuItem::findByMenuId($menu->id);
 
				// If one or more items have been found, pass them into the view file.
				if ($items !== false) {
					$view_data['items'] = $items;
				}
			}
 
			// Display the menu_index view file.
			$this->display('menu/views/menu_index', $view_data);
		}
	}
 
	// Process the index form.
	private function update() {
	}
 
	// Toggle the status of menu items.
	public function toggle_item($id) {
	}
 
	// Display the 'Add Menu' form.
	public function add_menu() {
	}
 
	// Process the 'Add Menu' form.
	private function _add_menu() {
	}
 
	// Display the 'Edit Menu' form.
	public function edit_menu($id) {
	}
 
	// Process the 'Edit Menu' form.
	public function _edit_menu($id) {
	}
 
	// Delete menu.
	public function delete_menu($id) {
	}
 
	// Display the 'Add menu item' form.
	public function add_item($menu_id=false) {
	}
 
	// Process the 'Add menu item' form.
	private function _add_item() {
	}
 
	// Display the 'Edit menu item' form.
	public function edit_item($id) {
	}
 
	// Process the 'Edit menu item' form.
	private function _edit_item($id) {
	}
 
	// Delete menu item.
	public function delete_item($id) {
	}
 
}

The CSS

Create a new file called ‘menu.css’ in the plugin directory and copy and paste the following into it:

#menu {
	margin:10px 0;
	font-size:13px;
}
 
/*
 * menu_index styles
 *
 */
#menu div.header {
	background:#483E37;
	padding:5px;
	height:20px;
}
 
#menu div.header div.title {
	font-weight:bold;
	padding:2px 0 0 5px;
	color:#fff;
	float:left;
}
#menu div.header ul {
	float:right;
	list-style:none;
	margin:0;
	padding:0;
}
#menu div.header ul li { float:right; margin:3px 8px 0 0; }
#menu div.header ul li a { color:#988060; }
#menu div.header ul li a:hover { color:#fff; }
 
#menu div.labels {
	background:#eee;
	border-bottom:1px solid #bbb;
	padding:8px 5px 19px 5px;
	color:#444;
}
 
#menu div.items,
#menu div.labels ul {
	list-style:none;
	margin:0;
	padding:0;
}
#menu div.items ul li div,
#menu div.labels ul li {
	margin:0 8px 0 0;
	float:left;
 
}
#menu div.labels ul li {
	text-transform:uppercase;
	font-weight:bold;
}
#menu div.items ul li div.right,
#menu div.labels ul li.right {
	float:right;
}
 
#menu div.items ul li {
	height:25px;
	padding-top:6px;
	border-bottom:1px solid #ccc;
}
#menu .position { width:70px; }
#menu .status { width:80px; }
#menu .delete { width:60px; }
#menu div.items div.position input {
	width:40px;
	text-align:center;
	border:1px solid #aaa;
	padding:1px 0px;
	margin-left:7px;
}
#menu div.items div.delete input {
	margin-top:3px;
	margin-left:15px;
	cursor:pointer;
}
#menu div.items {
	padding:8px 5px 19px 5px;
	position:relative;
}
#menu div.items #submit {
	float:right;
	margin-top:10px;
	cursor:pointer;
}
 
/*
 * sidebar styles
 *
 */
#menu-sidebar ul {
	list-style:none;
	margin:0 0 15px 0;
	padding:0;
	font-size:13px;
}
#menu-sidebar ul li {
	padding-left:10px;
	padding-top:5px;
	height:15px;
}
#menu-sidebar ul li.title {
	font-weight:bold;
	background:#483E37;
	padding:5px;
	height:15px;
	color:#fff;
}
#menu-sidebar ul li a {
	font-size:13px;
}

The Views

Now we come to writing our plugin view files!

views / sidebar.php We need a way of selecting what menu to open. We’ve already configured the index() method to allow us to select what menu to open by passing in the menu ID as an argument, but we need a frontend menu to display a list of menus allowing us to swap between different menus as we choose. Copy and paste the following into views/sidebar.php

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
/**
 * Get an array of all menus.
 *
 * You cant pass data into the sidebar view file so you
 * have to load any data in the file itself.
 */
$menus = Menu::find();
 
?>
<div class="box" id="menu-sidebar">
	<ul>
		<li class="title">Actions</li>
		<li><a href="<?php echo get_url('plugin/menu/add_menu'); ?>">Add Menu</a></li>
	</ul>
 
	<?php
	 /**
	  * The 'menus' menu allows you displays a list of menus which link
	  * to the index controller method with the menu id allowing you to
	  * view them.
	  */
	?>
	<ul>
		<li class="title">Menus</li>
		<?php foreach ($menus as $k => $menu): ?>
		<li><a href="<?php echo get_url('plugin/menu/index/'.$menu->id); ?>"><?php echo $menu->name; ?></a></li>
		<?php endforeach; ?>
	</ul>
</div>

views / menu_index.php This will be the view file for the default controller method index(). When you click on the plugin tab, this will be the first page you see.

The menu_index view file will list the current selected menu and its items inside a form that will allow us to update the menus items.

Create a new file called ‘menu_index.php’ in the views folder and copy and paste the following (comments provided).

<?php
 
if (!defined('IN_CMS')) { exit(); }
 
?>
 
<h1>Menus</h1>
 
	<?php
	/**
	 * If no menus exist, a menu object cannot be passed.
	 * In this instance, lets display a user friendly warning
	 * encouraging them to create a menu.
	 */
	if (!isset($menu) || !is_object($menu)):
	?>
		<p>No menus have been found. Why not <a href="<?php echo get_url('plugin/menu/add_menu'); ?>">create one</a>?</p>
 
	<?php
	/* A menu has been found. */
	else:
	?>
		<div id="menu">
			<div class="header">
				<div class="title"><?php echo $menu->name; ?></div>
				<ul>
					<li><a href="#" title="">Add Item</a></li>
					<li><a href="#" title="">Edit</a></li>
					<li><a href="#" title="">Delete</a></li>
				</ul>
			</div>
			<div class="labels">
				<ul>
					<li class="label">Label</li>
					<li class="delete right">Delete</li>
					<li class="position right">Position</li>
					<li class="status right">Status</li>
				</ul>
			</div>
	<?php
		/**
		 * If the menu has no items, again, display a user friendly warning.
		 */
		if (!isset($items) || (is_array($items) && empty($items))):
	?>
			<p>No menu items have been found for this menu. Why not <a href="<?php echo get_url('plugin/menu/add_item/'.$menu->id); ?>">create one</a>?</p>.
 
		<?php
		/* Items have been found, display the update form. */
		else:
		?>
			<form method="post" action="<?php echo get_url('plugin/menu/'); ?>">
				<div class="items">
					<ul>
						<?php
							/**
							 * Loop through menu items and display a row for each one
							 */
							foreach ($items as $k => $item):
						?>
 
						<li>
							<div class="label"><a href="<?php echo get_url('plugin/menu/edit_item/'.$item->id); ?>"><?php echo $item->label; ?></a></div>
							<div class="delete right"><input type="checkbox" name="item_delete[]" value="<?php echo $item->id; ?>"></div>
							<div class="position right"><input type="text" name="item_position[<?php echo $item->id; ?>]" value="<?php echo $item->position; ?>"></div>
							<div class="status right">
								<a href="<?php echo get_url('plugin/menu/toggle_item/'.$item->id); ?>">
								<?php if ($item->is_active == 'T'): ?>
									Enabled
								<?php else: ?>
									Disabled
								<?php endif; ?>
								</a>
							</div>
						</li>
 
						<?php endforeach; ?>
					</ul>
 
					<input type="submit" id="submit" value="Update">
				</div>
			</form>
		<?php endif; ?>
 
		</div><!-- #menus -->
 
	<?php endif; ?>

Now, if you havn’t already done so login to your Wolf installation’s backend area and enable the plugin. Click on the administration tab, then scroll down until you see menu, check the checkbox on the right hand side and you should see a new tab appear at the top. Click on the menu tab and you should have something like this: WolfCMS - Our menu plugin

WolfCMS - Our menu plugin

Thats it for part 1 of this tutorial, stay tuned for part 2. :)

In part 2 we will create the edit_menu and edit_menu view files as well as code the methods in the controller to create a functional, useful plugin.

 
tutorials/writing_a_plugin_1.txt · Last modified: 2012-11-06 14:09 by nowotny
 
Except where otherwise noted, content on this wiki is licensed under the following license:GNU Free Documentation License 1.2
Copyright 2010 wolfcms.org / design by yello studio / Wolf CMS Inside