User Tools

Site Tools


dev:crash-course

Plugin Development Crash Course

This is a short Oxwall plugin development crash course that is based on the existing Contact Us plugin. Let's see how it was created step by step.

Plugin files

So we begin. We need to create a contact form for sending members' feedback to site administration.

Create contactus folder (this is also our plugin key in this case) in the ow_plugins folder. This will be the plugin root folder where we will be performing all the work.

The plugin root folder should contain init.php file. Here we can add new routes and autoload class rules, bind events etc. Let's leave this file empty for now. This file will be executed every time your plugin is active.

Now let's create plugin.xml file - this file should contain our plugin information.

<?xml version="1.0" encoding="UTF-8"?>
<plugin>
	<name>Contact Us</name>
	<key>your_plugin_key_here</key>
	<description>"Contact us" page with the ability to choose departments (email addresses).</description>
    <author>Skalfa LLC</author>
    <authorEmail>plugins@oxwall.org</authorEmail>
	<authorUrl>http://www.skalfa.com</authorUrl>
	<developerKey>your_developer_key_here</developerKey>
    <build>1</build>
    <copyright>(C) 2009 Skalfa LLC. All rights reserved.</copyright>
	<license>The BSD License</license>
    <licenseUrl>http://www.opensource.org/licenses/bsd-license.php</licenseUrl>
</plugin>

Important: Note these strings:

	<key>your_plugin_key_here</key>
        ...
	<developerKey>your_developer_key_here</developerKey>
Those should be your plugin key and developer key, respectively. They should be correct, otherwise you may face problems upon submitting your plugin or auto-updating it in the admin area.

Now we should install and activate our plugin. For that we should create the following files in the root folder of our plugin: install.php, uninstall.php, activate.php and deactivate.php.

Add the following code to install.php file:

BOL_LanguageService::getInstance()->addPrefix('contactus', 'Contact Us');

We'll explain its functional purpose a bit later. Leave the rest of the files empty for now. That's it, our plugin is ready to be installed :)

Plugin installation

Yes, it's time to install your plugin before you actually started real development. Go to Admin area → Plugins → Available Plugins. Click Install on your new plugin.

Voila - our plugin has been successfully installed and activated. This should create necessary database records for the normal functioning of the new plugin.

Creating a page

Oxwall employs MVC pattern. Let's create two folders: controllers and views. Create one more controllers folder in the views folder. Create the first controller in the root controllers folder. Let's name it contact.php.

This file's content should be as follows:

class CONTACTUS_CTRL_Contact extends OW_ActionController 
{ 
}

Note: The name of the controller class should start with a prefix consisting of the plugin key (in upper case) and the word 'CTRL' separated by an underscore.

Note: All controllers should be inherited from OW_ActionController or its child classes.

Note: The file containing class declaration should be named according to the following rule - remove prefix from the class name, change the letters from upper to lower case following an underscore. For example, MYPLUGIN_CTRL_SuperPuper class will have a file named super_puper.php.

Let's create the first action inside of the class:

    public function index() 
    { 
        $this->setPageTitle("Contact Us"); 
        $this->setPageHeading("Contact Us"); 
    } 

An action is basically any class method with public access attribute.

Let's set page title and page heading inside of this method by calling standard methods of the OW_ActionController class.

Localization

In the above example we set page title directly, without using localization (languages mechanism).

For our contact form to support various languages go to the admin panel through the following URL <domain>/admin/dev-tools/languages. Note that this is a page available to developers only and it's more powerful than the standard language editing interface.

Click the Add New Text button. It will take you to a form for adding a new key. Choose the section whose name matches the name of the 'Contact Us' plugin (this section was added during the plugin installation with the code we inserted to the install.php file). Now, enter the key name index_page_title and the appropriate text. Add the index_page_heading key in the same way.

You can get text for the key in a current language by calling text() method of the Language object. Pass the plugin key and the language key as parameters. Change the following lines in the action code:

        $this->setPageTitle("Contact Us"); 
        $this->setPageHeading("Contact Us"); 
to the ones below:
        $this->setPageTitle(OW::getLanguage()->text('contactus', 'index_page_title')); 
        $this->setPageHeading(OW::getLanguage()->text('contactus', 'index_page_heading')); 

That's it. Now the title and heading can be displayed in any language.

Page routing

For our page to display correctly we should assign a view for the action. For that we need to create an empty file contact_index.html in the views/controllers/ folder. As you can see, the view name contains of the controller file name and the action name separated by an underscore.

Finally, we can take a look at our page. The URL of the page looks like this: <domain>/contactus/contact/index

That's what we are going to see by opening this URL in a browser:

Empty page

The URL looks rather lengthy. No worries though - Oxwall supports nice urls. For our page to be available from a shorter URL (from example, <domain>/contact) we should add the following line to the previously created init.php file:

OW::getRouter()->addRoute(new OW_Route('contactus.index', 'contact', "CONTACTUS_CTRL_Contact", 'index')); 

This line has added a new route.

Parameters:

  1. route name, contactus.index
  2. path
  3. controller class name
  4. name of the action the route points to.

It is desirable to make the route name compound, like this - <plugin_key>.<name>, because the route name should be unique. Now our page opens from the following address: <domain>/contact

But there is one small problem - the site users cannot see our page. To make the page accessible, let's add a link to the form to the bottom menu. For that we should create bottom_menu_item key in Languages with the prefix of our plug-in, and 'Contact us' as the value. Add the following code to the activate.php file:

OW::getNavigation()->addMenuItem(OW_Navigation::BOTTOM, 'contactus.index', 'contactus', 'bottom_menu_item', OW_Navigation::VISIBLE_FOR_ALL);

Also, add the code below to deactivate.php:

OW::getNavigation()->deleteMenuItem('contactus', 'bottom_menu_item');

Now go to the <domain>/admin/plugins page. Deactivate the plug-in, and then reactivate it. That's it. Now, when we enter the index page, we can see the “Contact us” element in the bottom menu.

Using forms

It's time to add a contact form to our page. Oxwall has a very convenient mechanism for working with forms.

To start with, let's declare an array of the contact emails that will be displayed on the contact form. Add to the index action of your controller (meaning index() method in contact.php) the following code:

$contactEmails = array(
  'admin@site.com' => 'Site administration',
  'support@site.com' => 'Technical support',
  'billing@site.com' => 'Billing department'
);

Let's also create a shortcut for calling OW::getLanguage→text():

private function text( $prefix, $key, array $vars = null )
{
  return OW::getLanguage()->text($prefix, $key, $vars);
}

Now you can use $this→text() for a short call.

The next step is creating the form object:

$form = new Form('contact_form');

We should add the following fields to the form: “to”(as a drop-down list), and three text fields: “from”, “subject” and “message”. Let's do this using the code below:

$fieldTo = new Selectbox('to');
foreach ( $contactEmails as $email => $label )
{
  $fieldTo->addOption($email, $label);
}
$fieldTo->setRequired();
$fieldTo->setHasInvitation(false);
$fieldTo->setLabel($this->text('contactus', 'form_label_to'));
$form->addElement($fieldTo);
 
$fieldFrom = new TextField('from');
$fieldFrom->setLabel($this->text('contactus', 'form_label_from'));
$fieldFrom->setRequired();
$fieldFrom->addValidator(new EmailValidator());
$form->addElement($fieldFrom);
 
$fieldSubject = new TextField('subject');
$fieldSubject->setLabel($this->text('contactus', 'form_label_subject'));
$fieldSubject->setRequired();
$form->addElement($fieldSubject);
 
$fieldMessage = new Textarea('message');
$fieldMessage->setLabel($this->text('contactus', 'form_label_message'));
$fieldMessage->setRequired();
$form->addElement($fieldMessage);
 
// Using captcha here to prevent bot spam
$fieldCaptcha = new CaptchaField('captcha');
$fieldCaptcha->setLabel($this->text('contactus', 'form_label_captcha'));
$form->addElement($fieldCaptcha);
 
$submit = new Submit('send');
$submit->setValue($this->text('contactus', 'form_label_submit'));
$form->addElement($submit);

Let's take a detailed look at adding form fields.

First we created the $fieldFrom object of the TextField type by setting the form element name in the constructor. We should set the field label using setLabel(). Don't forget to add keys for all the form fields using admin/dev-tools/languages page on your site.

Make the field required for filling in with setRequired(). One can add only a sender's email into this field, so let's add a standard email validator to the field by calling addValidator(). We can describe custom validators, and add them to the form elements. Please note that you can define your own form elements besides the standard ones by inheriting them from the FormElement class.

The last thing we should do here is add our element to the form using the addElement() form method. Pay attention to how easily we've added CAPTCHA by using the standard CaptchaField form element.

Now we should add our form to the controller by using the following code:

$this->addForm($form);

For the form to show up on the page, we should set its markup on the controller action view. Let's add the following code to the views/controllers/contact_index.html file:

{form name='contact_form'}
<table class="ow_table_1 ow_form ow_automargin ow_superwide">
<tr class="ow_alt1">
<td class="ow_label">{label name='to'}</td>
<td class="ow_value">{input name='to'}{error name='to'}</td>
</tr>
<tr class="ow_alt2">
<td class="ow_label">{label name='from'}</td>
<td class="ow_value">{input name='from'}{error name='from'}</td>
</tr>
<tr class="ow_alt1">
<td class="ow_label">{label name='subject'}</td>
<td class="ow_value">{input name='subject'}{error name='subject'}</td>
</tr>
<tr class="ow_alt2">
<td class="ow_label">{label name='message'}</td>
<td class="ow_value">{input name='message'}{error name='message'}</td>
</tr>
<tr class="ow_alt1">
<td class="ow_label">{label name='captcha'}</td>
<td class="ow_value ow_center">{input name='captcha'}{error name='captcha'}</td>
</tr>
<tr>
<td class="ow_center" colspan="2">{submit name='send' class='ow_button ow_ic_mail'}</td>
</tr>
</table>
{/form}

This is a usual HTML code with Smarty tags. This form is declared with an opening tag {form name=''} with a required name attribute and the closing tag {/form}. The form elements are declared with the following tags: {label name=''}, {input name=''} and {error name=''}. The name attribute is required for these tags.

In the end we should get the following page:

Contact form

Now it's time to make our form interactive and functional. Add the code below to the end of the index action:

if ( OW::getRequest()->isPost() )
{
  if ( $form->isValid($_POST) )
  {
    $data = $form->getValues();
 
    $mail = OW::getMailer()->createMail();
    $mail->addRecipientEmail($data['to']);
    $mail->setSender($data['from']);
    $mail->setSubject($data['subject']);
    $mail->setTextContent($data['message']);
    OW::getMailer()->addToQueue($mail);
 
    OW::getSession()->set('contactus.dept', $contactEmails[$data['to']]);
    $this->redirectToAction('sent');
  }
}

Let's see the code in details. The OW::getRequest()→isPost() call allows us to check whether the data sent by the form using the POST method have been received. Then the $form→isValid($_POST) checks validity of the $_POST array data, and adds the data to the form object. By calling $data = $form→getValues() we get the array containing the form fields. Next, we form a letter with the Mailer object, and add it to the mail-sending queue.

By calling OW::getSession() we get access to the session object. And set a variable to the session using the set() method. This variable is used in the sent action. Please pay attention to how the variable key is formed. It consists of the prefix (which is the plugin key) and the variable name separated by a dot. You should do that for your variable not to cross with other plugins' variables by any chance. By the final line $this→redirectToAction('sent') we redirect the user to the “sent” page.

Let's define the action for the “sent” page.

public function sent()
{
  $dept = null;
 
  if ( OW::getSession()->isKeySet('contactus.dept') )
  {
    $dept = OW::getSession()->get('contactus.dept');
    OW::getSession()->delete('contactus.dept');
  }
  else
  {
    $this->redirectToAction('index');
  }
 
  $feedback = $this->text('contactus', 'message_sent', ( $dept === null ) ? null : array('dept' => $dept));
  $this->assign('feedback', $feedback);
}

Here we should check if the variable is available in the session (the isKeySet session method), and if it's there - we get it (the get session method), and then delete it (the delete session method). Now we should add a dispatch notification text to the $feedback variable and add the variable to the template by using assign() method.

Also, create a message_sent key in Language with the following text “Your message has been sent. {$dept} will reply shortly. Thank you.”. Using {$dept} construction we set the dept key to the template. This key will be replaced by the value sent to the text method. Note that we passed the 3rd parameter in calling the text method. It is an array of the key ⇒ value kind. In our case the key name is dept, and the value is the variable received from the session.

Finally, let's create a view for the sent action. Create views/controllers/contact_sent.html file with the following content:

<div class="ow_center">{$feedback}</div>

We're done - now it's possible to send feedback to site administration through the contact form.

Using database

Our form has one shortcoming - the contact emails (departments) are hardcoded in index method. Wouldn't it be great to be able to change them via the admin panel?

Let's begin with creating backend for storing emails in the database. For that we should create bol folder (meaning 'Business Object Layer') in the plugin root folder. Let's create 3 files in this folder: department.php, department_dao.php, service.php. We should add DTO (Data Transfer Object) code to department.php:

class CONTACTUS_BOL_Department extends OW_Entity
{
    /**
     * @var string
     */
    public $email;
}

DTO is a kind of a table reflection. The rule for naming the DTO class and file is identical to the one for controllers except for one thing - CTRL suffix should be replaced with BOL. Our DTO should be inherited from OW_Entity class.

We should add DAO (Data Access Object) code to department_dao.php:

class CONTACTUS_BOL_DepartmentDao extends OW_BaseDao
{
 
    /**
     * Constructor.
     *
     */
    protected function __construct()
    {
        parent::__construct();
    }
    /**
     * Singleton instance.
     *
     * @var CONTACTUS_BOL_DepartmentDao
     */
    private static $classInstance;
 
    /**
     * Returns an instance of class (singleton pattern implementation).
     *
     * @return CONTACTUS_BOL_DepartmentDao
     */
    public static function getInstance()
    {
        if ( self::$classInstance === null )
        {
            self::$classInstance = new self();
        }
 
        return self::$classInstance;
    }
 
    /**
     * @see OW_BaseDao::getDtoClassName()
     *
     */
    public function getDtoClassName()
    {
        return 'CONTACTUS_BOL_Department';
    }
 
    /**
     * @see OW_BaseDao::getTableName()
     *
     */
    public function getTableName()
    {
        return OW_DB_PREFIX . 'contactus_department';
    }
}

DAO is required for interacting with database. Roughly speaking, DAO maps data from the table to DTO and back. The rule for naming the DAO class and file is identical to the one for controllers with one exception - CTRL suffix should be changed to BOL. Our DAO class is inherited from OW_BaseDao abstract class, and is a singleton. OW_BaseDao contains a number of useful methods for working with database.

Let's add service class code to service.php for working with departments:

class CONTACTUS_BOL_Service
{
    /**
     * Singleton instance.
     *
     * @var CONTACTUS_BOL_Service
     */
    private static $classInstance;
 
    /**
     * Returns an instance of class (singleton pattern implementation).
     *
     * @return CONTACTUS_BOL_Service
     */
    public static function getInstance()
    {
        if ( self::$classInstance === null )
        {
            self::$classInstance = new self();
        }
 
        return self::$classInstance;
    }
 
    private function __construct()
    {
 
    }
 
    public function getDepartmentLabel( $id )
    {
        return OW::getLanguage()->text('contactus', $this->getDepartmentKey($id));
    }
 
    public function addDepartment( $email, $label )
    {
        $contact = new CONTACTUS_BOL_Department();
        $contact->email = $email;
        CONTACTUS_BOL_DepartmentDao::getInstance()->save($contact);
 
        BOL_LanguageService::getInstance()->addValue(
            OW::getLanguage()->getCurrentId(),
            'contactus',
            $this->getDepartmentKey($contact->id),
            trim($label));
    }
 
    public function deleteDepartment( $id )
    {
        $id = (int) $id;
        if ( $id > 0 )
        {
            $key = BOL_LanguageService::getInstance()->findKey('contactus', $this->getDepartmentKey($id));
            BOL_LanguageService::getInstance()->deleteKey($key->id, true);
            CONTACTUS_BOL_DepartmentDao::getInstance()->deleteById($id);
        }
    }
 
    private function getDepartmentKey( $name )
    {
        return 'dept_' . trim($name);
    }
 
    public function getDepartmentList()
    {
        return CONTACTUS_BOL_DepartmentDao::getInstance()->findAll();
    }

This class is also a singleton, which allows us to save server resources. The difference between service and DAO is that service calls methods from DAO (not knowing how data is stored) and executes other business logic. There is no business logic in DAO, there are only database queries.

Let's take a look at getDepartmentList method of CONTACTUS_BOL_Service class. We get instance of CONTACTUS_BOL_DepartmentDao class in this method, and call standard findAll method that returns an array of CONTACTUS_BOL_Department objects.

Now let's study addDepartment method of the same class, which is a bit more interesting. First we should create CONTACTUS_BOL_Department class object. Then we should set an email value in it, and save the ready object using save method of CONTACTUS_BOL_DepartmentDao class. Now let's add a label for the new-created department to Languages. This can be done with addValue method of BOL_LanguageService class instance.

In order to create a department table you should add the following code to the end of install.php file:

$sql = "CREATE TABLE `" . OW_DB_PREFIX . "contactus_department` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `email` VARCHAR(200) NOT NULL,
    PRIMARY KEY (`id`)
)
ENGINE=MyISAM
ROW_FORMAT=DEFAULT";
 
OW::getDbo()->query($sql);

Important: Pay attention to OW_DB_PREFIX constant - this is the table prefix you entered during the installation. Always use it when creating or querying tables.

Important: Always name tables according to the following template : <pluginkey>_<tablename> - this will prevent your plugin tables from crossing with other plugin tables.

By calling OW::getDbo we get OW_Database object for working with database which is of lower level than OW_BaseDao. Class OW_Database is a wrapper for PDO. In order to perform a database query we use query method of OW_Database class having passed an sql query as a parameter.

Go to <domain>/admin/plugins page, backup your plugin languages (important!), uninstall, and then reinstall our plugin. After that contactus_department table will be created.

Creating Settings page in Admin area

It's time to create an admin panel page for managing departments. Create admin.php file in controllers folder of our plugin. Add the controller code to this file:

class CONTACTUS_CTRL_Admin extends ADMIN_CTRL_Abstract
{
 
    public function dept()
    {
        $this->setPageTitle(OW::getLanguage()->text('contactus', 'admin_dept_title'));
        $this->setPageHeading(OW::getLanguage()->text('contactus', 'admin_dept_heading'));
        $contactEmails = array();
        $deleteUrls = array();
        $contacts = CONTACTUS_BOL_Service::getInstance()->getDepartmentList();
        foreach ( $contacts as $contact )
        {
            /* @var $contact CONTACTUS_BOL_Department */
            $contactEmails[$contact->id]['name'] = $contact->id;
            $contactEmails[$contact->id]['email'] = $contact->email;
            $contactEmails[$contact->id]['label'] = CONTACTUS_BOL_Service::getInstance()->getDepartmentLabel($contact->id);
            $deleteUrls[$contact->id] = OW::getRouter()->urlFor(__CLASS__, 'delete', array('id' => $contact->id));
        }
        $this->assign('contacts', $contactEmails);
        $this->assign('deleteUrls', $deleteUrls);
 
        $form = new Form('add_dept');
        $this->addForm($form);
 
        $fieldEmail = new TextField('email');
        $fieldEmail->setRequired();
        $fieldEmail->addValidator(new EmailValidator());
        $fieldEmail->setInvitation(OW::getLanguage()->text('contactus', 'label_invitation_email'));
        $fieldEmail->setHasInvitation(true);
        $form->addElement($fieldEmail);
 
        $fieldLabel = new TextField('label');
        $fieldLabel->setRequired();
        $fieldLabel->setInvitation(OW::getLanguage()->text('contactus', 'label_invitation_label'));
        $fieldLabel->setHasInvitation(true);
        $form->addElement($fieldLabel);
 
        $submit = new Submit('add');
        $submit->setValue(OW::getLanguage()->text('contactus', 'form_add_dept_submit'));
        $form->addElement($submit);
 
        if ( OW::getRequest()->isPost() )
        {
            if ( $form->isValid($_POST) )
            {
                $data = $form->getValues();
                CONTACTUS_BOL_Service::getInstance()->addDepartment($data['email'], $data['label']);
                $this->redirect();
            }
        }
    }
 
    public function delete( $params )
    {
        if ( isset($params['id']) )
        {
            CONTACTUS_BOL_Service::getInstance()->deleteDepartment((int) $params['id']);
        }
        $this->redirect(OW::getRouter()->urlForRoute('contactus.admin'));
    }
}

Pay attention to the line in dept method:

$contacts = CONTACTUS_BOL_Service::getInstance()->getDepartmentList();
This line gets us a list of all the available departments. Then we should form the emails array for displaying them as a table.

Pay attention to the line:

OW::getRouter()->urlFor(__CLASS__, 'delete', array('id' => $contact->id));
Using urlFor method of OW_Router class we can generate URL to delete action of our controller with GET parameter id.

Now let's create a view for dept action. For that we should create views/controllers/admin_dept.html file with the following content:

<table class="ow_table_1 ow_automargin" style="width: 400px;">
    {foreach from=$contacts item=contact}
    <tr class="{cycle values='ow_alt1,ow_alt2'}">
        <td width="1"><a href="{$deleteUrls[$contact.name]}" onclick="return confirm('{text key="base+are_you_sure"}');" style="width:16px; height:16px; display:block; margin:0 auto;background-repeat:no-repeat;background-position: 50% 50%;" class="ow_ic_delete"></a></td>
        <td>{$contact.email}</td>
        <td>{$contact.label}</td>
    </tr>
    {/foreach}
</table>
 
{form name='add_dept'}
<table class="ow_table_1 ow_form ow_automargin" style="width: 400px;">
    <tr class="ow_alt1">
        <td class="ow_value">{input name='email'}</td>
        <td class="ow_value">{input name='label'}</td>
    </tr>
    <tr>
        <td class="ow_center" colspan="2">{submit name='add' class='ow_button ow_ic_save'}</td>
    </tr>
</table>
{/form}

Pay attention to {text key=“base+are_you_sure”} construction - this is a Smarty function for displaying localized text (synonym for OW::getLanguage→text). We should add prefix and language key separated by “+” symbol to key parameter.

Now our page for departments configuration is available at the following URL: <domain>/contactus/admin/dept. Having added a couple of departments we will see the following page:

Department configuration

Data is saved in the database, but are not displayed on the contact form. In order to display them on the contact form get back to controllers/contact.php file and index method. Replace the lines:

$contactEmails = array(
    'admin@site.com' => 'Site administration',
    'support@site.com' => 'Technical support',
    'billing@site.com' => 'Billing department'
);

with these lines:

$contactEmails = array();
$contacts = CONTACTUS_BOL_Service::getInstance()->getDepartmentList();
foreach ( $contacts as $contact )
{
    /* @var $contact CONTACTUS_BOL_Department */
    $contactEmails[$contact->email] = CONTACTUS_BOL_Service::getInstance()->getDepartmentLabel($contact->id);
}

Our plugin is almost ready. All we need to do is to add the link to the departments configuration page to our plugin settings in the admin panel. For that you should add the following line to the end of init.php file:

OW::getRouter()->addRoute(new OW_Route('contactus.admin', 'admin/plugins/contactus', "CONTACTUS_CTRL_Admin", 'dept'));

This line has made our page available at the URL: <domain>/admin/plugins/contactus

Add the line below to the end of install.php file:

OW::getPluginManager()->addPluginSettingsRouteName('contactus', 'contactus.admin');

Uninstall the plugin (again, don't forget to save your language pack!) and then reinstall it. Now if you go to <domain>/admin/plugins page you will see 'Settings' button next to our plugin name.

Plugin settings

By clicking this button we get directed to the departments edit page.

Note: if you added tables and languages following the above rules - they will be automatically deleted at uninstallation.

That's it, our plugin is ready :) You've just survived Oxwall plugin development crash course. Go create with us!


Send feedback about this article

dev/crash-course.txt · Last modified: 2015/09/15 09:32 by Den