Writing your own Finder plugin - Part 1

DRAFT - WORK IN PROGRESS

This is part 1 in a series that will outline how to design your own search plugin for Finder.  In this tutorial we look at building the indexer for a particular type of content.

Please note that this information is provided for informational purposes only.  Support for creating new Finder plugins is not covered under regular support subscription.  Additional fees for assistance can be negotiated on a case-by-case basis.

You must have a good working of PHP and Joomla extension development to attempt this tutorial.

Setting the Scene

This example will be based on a real-life project for integrating information from software called Stone Orchard into Joomla.  Stone Orchard is a SQL Server based application for managing cemetery burials.  Joomla was chosen to act as a web display mechanism for the project which involved caching a table of information in MySQL.  It is this table that we will be wanting Finder to index.

The completed source code for this plugin and be found in the GraveFinder project at JoomlaCode.org.  Note that this is the live instance and the code may differ from the examples given in the rest of this tutorial.

Creating the Plugin

The first step in the process is to create the shell for the Joomla plugin.  We need to create two files in the Finder plugin folder:

/plugins/finder/stone_burials.php
/plugins/finder/stone_burials.xml

and a translation file:

/administrator/language/en-GB/en-GB.plg_finder_stone_burials.php

The translation file is used to store text that is used in the metadata file.

The Plugin Metadata File

At this stage of development, the metadata file stone_burials.xml is reasonably simple.

<?xml version="1.0" encoding="utf-8"?>
<install
    version="1.5.14"
    type="plugin"
    group="finder">
    <name>Finder - Stone Orchard Burials</name>
    <group>finder</group>
    <version>1.0.0</version>
    <creationDate>25 August 2009</creationDate>
    <author>Andrew Eddie</author>
    <authorEmail>
  This e-mail address is being protected from spambots. You need JavaScript enabled to view it
 </authorEmail>
    <authorUrl>http://www.newlifeinit.com</authorUrl>
    <copyright>2009 New Life in IT Pty Ltd. All rights reserved.</copyright>
    <description>Stone_Burials_Finder_Plugin_Desc</description>
    <files>
        <filename
            plugin="stone_burials">stone_burials.php</filename>
    </files>
</install>

This is a very typical metadata file for a Joomla plugin.  No parameters are needed yet.

The Main Plugin File

For now, we will just construct a shell.

<?php
/**
 * @version      $Id: catalog_nodes.php 357 2009-01-29 11:02:04Z robs $
 * @copyright    Copyright (C) 2009 New Life in IT Pty Ltd. All rights reserved.
 * @license      GNU General Public License version 2 or later
 * @link         http://www.newlifeinit.com
 */

defined('JPATH_BASE') or die;

require_once dirname(__FILE__).DS.'_prototype.php';

$lang = &JFactory::getLanguage();
$lang->load('plg_finder_stone_burials');

/**
 * Finder adapter for Stone Orchard (for Joomla) burial information.
 *
 * @package       NewLife.Stone
 * @subpackage    plg_finder_stone_burials
 */
class plgFinderStone_Burials extends plgFinderPrototype
{
	/**
	 * @var	string	The data table for the items to be indexed
	 */
	var $_src_table = '#__stone_burials';

	/**
	 * @var	string	An abitrary but unique context for this plugin
	 */
	var $_context = 'Stone_Burials';

	/**
	 * @var	string	The name of the item being indexed. Found under the "Type" Content Map
	 */
	var $_type_title = 'Burial';

	/**
	 * @var	string	The alias of the item being indexed. Found under the "Type" Content Map
	 */
	var $_type_alias = 'burial';
}

Following the standard prologue for a Joomla file (the DocBlock and the direct access preventive), we load a plugin prototype upon which we will base our plugin class.  The language file for the plugin is also loaded by hand as this is not done automatically by the Joomla framework.

The plugin class must follow the standard naming convension for Joomla plugins.

Now we need to define four variable that integrate with Finder.

  • _src_table - This is the name of the table that holds the source data that we are going to index.
  • _context - This is a unique label that Finder uses internally so that the information this plugin uses doesn't get confused with any other Finder plugin.
  • _type_title - This is the title of the type of content that is being indexed.  This gets added under the Type content map branch.
  • _type_alias - This is the alias for the type of content being indexed (it's simply the SEF representation of the type title).

The Translation File

For now, we just need the translation file, en-GB.plg_finder_stone_burials.ini, to hold the localized description.

# $Id$
# Copyright    (C) 2009 New Life in IT Pty Ltd. All rights reserved.
# License    GNU General Public License
# Note : All ini files need to be saved as UTF-8

STONE_BURIALS_FINDER_PLUGIN_DESC=This plugin indexes the Stone Orchard Burial cache in Joomla for use with Finder.

Installing the Plugin

You can either package these files together and install them in the normal Joomla way, or create them in-situ and then run the following database query to add the plugin to the database manually:

INSERT INTO `jos_plugins` VALUES
(0 , 'Finder - Stone Orchard (for Joomla) Burials', 'stone_burials', 'finder', '0', '0', '1', '0', '0', '0', '0000-00-00 00:00:00', ''); 

Whichever method you choose, you should check that the plugin is showing in the Joomla Plugin Manager.

Designing the Indexer Methods

There are now a number of methods we need to add to the plugin in order for it to start indexing the content.

The _setup Method

The setup method is used to add any custom branches to the Finder content maps (examples of branches are categories and sections in Joomla articles).

    /**
     * Override setup to initialize custom Branches, etc
     *
     * @return    boolean
     */
    function _setup()
    {
		// Ensure the branches are added before we start
		if ($this->_taxonomy->addBranch('Cemetery', 'cemetery', 1, 0) === false) {
			$this->_subject->setError($this->_taxonomy->getError());
			return false;
		}
		if ($this->_taxonomy->addBranch('Gender', 'gender', 1, 0) === false) {
			$this->_subject->setError($this->_taxonomy->getError());
			return false;
		}
        return true;
    }

The main point of this method is to add the "Cemetery" branch (with an alias of "cemetery").  The last two arguments of the addBranch method are the published state and view access level respectively.  Adding a branch like this will allow us to filter by the cemetery in the Finder advanced search.

You can add any number of branches as are required to suit your content.  If you do not need to add any branches then you can omit this method, or include it and simple have it return true.

The _getUrl Method

The next method we add is the method that returns the base URL for the content given the ID for the content.

    /**
     * Gets the URL for the link for a given Id
     *
     * @param    int $id        The id of the burial record.
     *
     * @return    string
     */
    function _getUrl($id)
    {
        return 'index.php?option=com_stone&view=burial&id='.(int) $id;
    }

 

The _getListQuery Method

This method does the job of retrieving the content data that to index.  This is a fairly simple example because we are bringing back all the fields in the table and there is no related information in other tables.

    /**
     * The query for getting the list of items
     *
     * @return JxQuery
     */
    function &_getListQuery()
    {
        jximport('jxtended.database.query');

        $query = new JXQuery;
        $query->select('a.*');
        $query->from('#__stone_burials AS a');

        return $query;
    }

The method just returns a query that Finder will process during the indexing process.

The _indexItem Method

This method is the workhorse that takes the content data and assembles it in the right way for Finder to index.

    /**
     * Index a burial
     *
     * @param    object
     *
     * @return    boolean
     */
    function _indexItem(&$item)
    {
        jimport('joomla.application.router');
        require_once JPATH_SITE.DS.'includes'.DS.'application.php';
        require_once JPATH_SITE.DS.'components'.DS.'com_stone'.DS.'router.php';

        // We are routing the URL so that we can score the route.
        $url    = $this->_getUrl($item->id);
        $route    = StoneRoute::burial($item->id);
        $sef    = JXIndexerHelper::getContentRoute($route);

        $misc            = array();
        $misc['meta']    = array();

        // To improve search results, add associated data to be indexed (but don't store it).
        if ($item->Undertaker_Last_Name) {
            $misc['meta'][]    = $item->Undertaker_Last_Name;
        }

        // Build the title.
        $title = strtoupper($item->Family_Name);
        if ($item->Family_Name)
        {
            if (!empty($title)) {
                $title .= ', ';
            }
            $title .= $item->Given_Names;
        }

        // Build the text to index.
        $text = 'Age: '.$item->Age_at_Death;
        $text .= ', Gender: '.$item->Age_at_Death;
        $text .= ', Died: '.$item->Date_of_Death;
        $text .= ', Buried: '.$item->Date_of_Burial;
        $text .= ', Cemetery: '.$item->Cemetery;
        $text .= ', Location: '.$item->Location;

        // Index the item
        $linkId = JXIndexerHelper::index(
            $url,
            $route,
            $sef,
            $title,
            '',                    // Meta description
            '',                    // Meta keywords
            $text,                // The text to index
            1,                    // The published state
            $this->_type_id,
            0,                    // The access level
            0,
            $misc,
            $this->_config,
            null,                    // Publishing Start Date
            null,                    // Publishing End Date
            $item->Date_of_Birth,    // Content State Date
            $item->Date_of_Burial    // Content End Date
        );

        // Check for errors.
        if (JError::isError($linkId)) {
            $this->_subject->setError($linkId->getMessage());
            return false;
        }

        // Remove the existing taxonomy maps
        if (!$this->_taxonomy->removeMaps($linkId)) {
            $this->setError($this->_taxonomy->getError());
            return false;
        }

        if (!$this->_indexTaxonomy($item, $linkId)) {
            return false;
        }

        return true;
    }

 

The _indexTaxonomy Method

Finally, we add the method to index the taxonomy mappings.

    /**
     * Index the related taxonomy
     *
     * @param    object    The item being indexed
     * @param    int        The current link ID that the taxonomy relates to
     * @return    boolean
     */
    function _indexTaxonomy(&$item, $linkId)
    {
        // Add the class taxonomy map
        if ($item->Cemetery)
        {
            if (!$this->_addMap($linkId, 'Cemetery', $item->Cemetery, '', 1, 0)) {
                return false;
            }
        }
        if ($item->Gender)
        {
            if (!$this->_addMap($linkId, 'Gender', $item->Gender, '', 1, 0)) {
                return false;
            }
        }

        return parent::_indexTaxonomy($item, $linkId);
    }