Developing a plugin for Webasyst app

Tutorials » #2. Developing Brands plugin for Shop-Script from scratch

This is a detailed tutorial on developing a plugin for a Webasyst app on the example of a plugin similar to Brands for Shop-Script.

During the development, it is recommended to take the requirements into account which are applicable to publication of software products in Webasyst Store.

0. Developer mode

Before you proceed to creating your own plugin, ensure that the developer mode (debug_mode) is enabled in your Webasyst account configuration. In this mode any caching is disabled to facilitate debugging. You can enable developer mode in the settings of the Installer app or in file wa-config/config.php as shown below:

'debug' => true

1. File structure

First of all create an id for your plugin. You may use basic English letters, digits, or underscore characters in the plugin id. For a plugin named Brands we shall take id brands. This id will be used for creating names of plugin files and PHP classes.

All source files of a plugin are stored inside a separate subdirectory inside wa-apps/[app_id]/plugins/. In our case files of the Brands plugin for Shop-Script must be saved inside wa-apps/shop/plugins/brands/. The paths to various directories and files will be specified in this article relative to that base directory.

The plugin directory must contain the following mandatory items:

Other files and directories will be created depending on the plugin's functionality.

2. Configuration

In file lib/config/plugin.php the following required parameters must be specified:

<?php

return array(
    'name' => 'Brands', // plugin name
    'img'  => 'img/brands.png' // relative path to the plugin icon file (16*16px), usually located in the img/ subdirectory
);

The name and icon are displayed in the “Plugins” section of the Shop-Script backend.

In addition to those 2 parameters, there may also be other parameters in that file:

Enable plugin

To enable the plugin, add an entry with its ID to configuration file wa-config/app/[app_id]/plugins.php as shown in the example:

<?php

return array(
    'import' => true,
);

3. Functionality

Our further actions will depend on what exactly the plugin should be able to do. There are 2 basic types of actions a plugin is able to perform:

  1. Add new (or modify existing) content to a web page, in the backend or frontend.
  2. Process users' requests (GET/POST/AJAX).

Let the Brands plugin display a list of links to available brands, and online shoppers will be able to click on them to view all products associated with the selected brand. The will require implementing both of the above-mentioned types of plugin functionality: 1) add new content to the storefront (a list of brands in the navigation panel), and 2) process user requests (generate pages with product lists corresponding to the selected brand name).

Adding new content (brand list)

For adding content to app's backend or frontend, it is recommended to use hooks, i.e. special points in the app's source code to which a plugin can connect to perform additional actions. The list of available hooks for Shop-Script is published in the documentation. For example, to add the list of brands to the storefront navigation panel, it is best to use hook frontend_nav.

In order to connect a plugin to a hook, add parameter 'handlers' to plugin's main configuration file lib/config/plugin.php and specify the list of hooks you need to connect your plugin to:

'handlers' => array(
    'frontend_nav' => 'frontendNav',
),

In our example, the plugin will be connected to only one hook, frontend_nav.

Hook ids must be specified in array keys. Each item value must contain the name of a method (in this example frontendNav) of the plugin's main class, which must be created at the path of the form lib/[app_id][Plugin_id].plugin.php. Hence, for our Brands plugin main class file must be created at lib/shopBrands.plugin.php. This file must contain class named shopBrandsPlugin extending base class shopPlugin.

Actually you may extend system base class waPlugin instead, but we recommend using shopPlugin when developing plugins for Shop-Script, because it adds some useful functionality.

In the main plugin class, describe method named exactly as specified in configuration file (frontendNav in this example).

There are no special naming rules for class methods in addition to those applied to entire framework. In particular, it is not required to name class methods similar to the hook id it is connected to. It is only essential to ensure that the method name in class file is identical to that specified in configuration file plugin.php.

Below is shown an example of the main class file containing method frontendNav connected to hook frontend_nav:

<?php


class shopBrandsPlugin extends shopPlugin
{

    public function frontendNav()
    {
        $html = ...; //here we generate HTML code of the brand list to be embedded in the frontend
        return $html;
    }

}

Values returned by plugin class methods

Plugin methods connected to Shop-Script hooks can return values in different ways depending on the type of the specific hook:

  1. a single value is returned (e.g., a string of HTML code as in the latter example)
  2. an array of values is returned (if a hook allows adding various values to different parts of a web page using one plugin method)
  3. no value is returned (e.g., if processing a hook requires only to execute a query to the database or add a record to a log file without displaying anything on a web page).

Below are shown examples for all these cases:

<?php


class shopBrandsPlugin extends shopPlugin
{

    //1) a single value is returned (when connected to hook frontend_nav)
    public function frontendNav()
    {
        $html = ...; //HTML code is generated to be embedded on a web page
        return $html;
    }

    //2) an array of values is returned (when connected to hook frontend_product)
    public function frontendProduct()
    {
        //in this method an array of HTML code snippets is generated which must be displayed on a web page
        //each of those several snippets will be embedded in the appropriate place allocated in the corresponding design theme template (if it's a frontend hook; e.g., frontend_product) or a backend template
        $data = array(
            'menu' => '...', //HTML code snippet
            'cart' => '...', //HTML code snippet
            'block' => '...', //HTML code snippet
            'block_aux' => '...', //HTML code snippet
        );
        return $data;
    }

    //3) no value is returned (when connected to hook category_delete)
    public function categoryDelete()
    {
        //if a method is connected to hook category_delete, you can execute various operations in this method when a product category is deleted (e.g., execute a database query) without returning any value, because this type of hooks simply ignores any returned values
    }

}

In this example, for the purpose of simplicity, we are dealing only with the first case when a single value (an HTML code snippet) is returned by a method.

Adding content without the use of hooks

The above-described method of adding custom content to a web page by means of (interface) hooks is the recommended one. Interface hooks allow adding custom content in a limited number of places of frontend and backend pages.

If you feel absolutely necessary to add custom content in other places, then you cannot use hooks to do so. In this case you can call static methods of plugin's classes (e.g., main plugin class) in the design theme. For example, you may want to display information about product's brand on the product-viewing page, then you can create a public static method for this purpose as shown below:

<?php


class shopBrandsPlugin extends shopPlugin
{

    public function frontendNav()
    {
        ...;
    }

    public static function getProductBrand($product_id)
    {
        $brand_name = ...; //obtaining brand name for the product with specified id
        return $brand_name;
    }

}

To display the desired value in the storefront, add a call of this method in the design theme source code as shown below:

{shopBrandsPlugin::getProductBrand($product_id)}

Note that this method of adding custom content to frontend pages is not recommended because it requires that users manually modify the source code of their design theme (by adding a call of a static method to the required location). Not all users are qualified enough to complete this task correctly; therefore, we discourage you to use this option as long as you can in favor of connecting your plugin to app's hooks.

For example, you may try to implement required functionality by means of hooks allowing you to include custom JavaScript files and add custom content using JavaScript.

Remember that calling static methods of a plugin is possible only in the frontend through editing design theme templates. Backend pages cannot be modified in the same way, because backend templates cannot be modified using web-based user interface provided by Shop-Script.

User request processing (creation of product-listing pages)

Requests from the users of Webasyst frontend and backend are processed by special PHP classes called controllers. Requests sent to different URLs can be processed by different or several common controllers as described below.

The appropriate controller for processing user requests to an app's backend is determined by Webasyst framework according to the rules described in the documentation. Important note for plugins: when a request is sent to the backend of a plugin, you have to add parameter of the form plugin=[plugin_id] to the request URL. For example, the request URL to the backend of plugin Brands may have the following form: http://yourdomain.com/webasyst/shop/?plugin=brands&action=someaction.

Should you, however, as required in our example, need to process user requests in the frontend, i.e. online storefront, then the rules determining the correct controller for a request must be specified in a separate configuration file lib/config/routing.php.

First of all, we need to define the URL mask for pages which will display product lists associated with a selected brand. Let it be this: http://STOREFRONT_URL/brand/<brand name>/. The part inside angle brackets is dynamic, i.e. individual for each brand. Now we add the following record to file routing.php:

<?php

return array(
    'brand/<brand>/' => 'frontend/brand', //'relative URL mask' => 'module/action combination'
    
    '.../' => '.../...', //you can add more routing rules if necessary
);

The dynamic part of the URL mask contains the name of the parameter (<brand>), which the plugin will use to extract the brand name from the request URL. The value of each array item in the configuration file must contain the combination of module and action which must be used to process user requests sent to the specified URL.

Array keys must contain relative URLs without a slash at the beginning! It is required for request URLs to be formed on the basis of the root URL of the current storefront (settlement).

Now we have to create the PHP class for the action corresponding to combination frontend/brand according to rule lib/actions/[app_id][Plugin_id]Plugin[Module][Action].action.php. In our example we will create file lib/actions/shopBrandsPluginFrontendBrand.action.php. In this file we shall declare a class extending base class shopFrontendAction (if you need to support common functionality implemented in other frontend actions of Shop-Script; otherwise you may want your action class to inherit system class waViewAction) with the corresponding name shopBrandsPluginFrontendBrandAction, and describe the product list generation logic in its method execute:

<?php

 
class shopBrandsPluginFrontendBrandAction extends shopFrontendAction

{

    public function execute()

    {

        $brand = waRequest::param('brand'); //in the action class, you can retrieve dynamic URL part (<brands> in this example) by calling system method waRequest::param($param_name)
        
        //other dynamic URL parts, if described in file routing.php, can be retrieved in the same way
        
        ...//then generate a product list corresponding to the retrieved dynamic URL part
    }

 
}

Processing user requests without using action classes

in addition to view actions, you can also process user requests by means of controllers; e.g., in case of AJAX requests.

Suppose we want to display a popup area with a product list instead of opening a page. To do so, send a GET request using JavaScript to a URL of the form http://STOREFRONT_URL/brands/<brand>/ and use the response received from the server to create and display a popup area with a poduct list.

In this case we need that such requests are not processed by an action class extending base class waViewAction (or shopFrontendAction) and are instead passed on to a controller implementing arbitrary logic. So, instead of creating file lib/actions/shopBrandsPluginFrontendBrand.action.php, we shall create file lib/actions/shopBrandsPluginFrontendBrand.controller.php. In this file we must describe a class extending system class waJsonController and named shopBrandsPluginFrontendBrandController. Its method execute will contain the AJAX request processing logic as described in the documentation; e.g.:

<?php

 
class shopBrandsPluginFrontendBrandController extends waJsonController

{

    public function execute()

    {

        $brand = waRequest::param('brand'); //retrieve a brand name from the request URL
        ... //create a product list corresponding to the retrieved brand name
        ... //generate HTML code or create an array of data to be used by JavaScript code to display products in a popup area
        $this->response = array(
            'result' => ... //return prepared data in response to the AJAX request
        );
    }

 
}

4. Templates, localization, CLI

A plugin, like a app, can utilize its own template files to generate HTML code, can have its own localization strings to be displayed in storefronts with different locales, and can offer its own CLI methods for cron job setup. Read in detail about these features in article “Plugin development basics”.