====== Plugin Development Crash Course ====== This is a short Oxwall plugin development crash course that is based on the existing [[http://www.oxwall.org/store/item/13|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 [[dev:faq:why-do-i-need-the-plugin-key|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. Contact Us your_plugin_key_here "Contact us" page with the ability to choose departments (email addresses). Skalfa LLC plugins@oxwall.org http://www.skalfa.com your_developer_key_here 1 (C) 2009 Skalfa LLC. All rights reserved. The BSD License http://www.opensource.org/licenses/bsd-license.php **Important**: Note these strings: your_plugin_key_here ... your_developer_key_here Those should be your [[dev:plugin-key|plugin key]] and [[dev:developer-key|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 [[http://en.wikipedia.org/wiki/Model-View-Controller|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 [[dev:plugin-key|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 **/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: **/contactus/contact/index** That's what we are going to see by opening this URL in a browser: {{:dev:contact1.png|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, **/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**: - route name, **contactus.index** - path - controller class name - name of the action the route points to. It is desirable to make the route name compound, like this - **.**, because the route name should be unique. Now our page opens from the following address: **/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 **/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'}
{label name='to'} {input name='to'}{error name='to'}
{label name='from'} {input name='from'}{error name='from'}
{label name='subject'} {input name='subject'}{error name='subject'}
{label name='message'} {input name='message'}{error name='message'}
{label name='captcha'} {input name='captcha'}{error name='captcha'}
{submit name='send' class='ow_button ow_ic_mail'}
{/form}
This is a usual HTML code with [[http://www.smarty.net/|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: {{:dev:contact2.png|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 [[dev:plugin-key|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:
{$feedback}
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 ([[http://en.wikipedia.org/wiki/Data_Transfer_Object|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 ([[http://en.wikipedia.org/wiki/Data_access_object|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 [[http://en.wikipedia.org/wiki/Singleton_pattern|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 : **_** - 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 [[http://php.net/manual/en/book.pdo.php|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 **/admin/plugins** page, backup your [[dev:language-packs|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: {foreach from=$contacts item=contact} {/foreach}
{$contact.email} {$contact.label}
{form name='add_dept'}
{input name='email'} {input name='label'}
{submit name='add' class='ow_button ow_ic_save'}
{/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: **/contactus/admin/dept**. Having added a couple of departments we will see the following page: {{:dev:contact3.png|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: **/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 [[dev:language-packs|language pack]]!) and then reinstall it. Now if you go to **/admin/plugins** page you will see 'Settings' button next to our plugin name. {{:dev:contact4.png|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! ---- [[http://www.oxwall.org/contact|Send feedback]] about this article