Creating Extension from Scratch

From osCommerce Wiki
Jump to navigation Jump to search

Intro

Extension is the tool that allows to deploy not only the minor basic function modifications, but also create the independent extensions based on the system. In this lecture we will review all the extension elements in detail and deploy them in our first extension on practice. We recommend to get familiarized with this whole article. Even without having the great knowledge of the system you will be able to create your own extension sooner or later. But following the general principles will allow the majority of developers to understand your code quickly and easily.

First look

Conceptually extension is the system mini copy. By engaging all its means you will be able to create the frontend analogue easily or create the report for backend, based on the data analysis.  

Image 1

On the image 1 (above) you can see the general architecture scheme of the extension constituent parts. For easy perception all the parts are divided into 4 categories.

The founding elements are indicated as the Basic functionality. This is the minimum set that we will require to perform the simplest manipulations within the system.

First of all, the functions are the static methods, realized in the main extension class or the traits if your extension allows it. Usually, the methods are not intended for querying directly, but, if necessary, backend allows to do it via the special controller extensions?module=YourExtensionName&action=adminAction. For frontend such approach is considered to be unsafe and is unavailable.

The next founding element is the hooks. These are the access points, allowing to execute your code in different places. Hooks can be as both the php file and the tpl template. As a rule, the similar files are placed in the subdirectory hooks and are described in the static method getAdminHooks in the main class or in the installing class which we will review later. You can always find the list of the available hooks within the system by checking the file /lib/common/extensions/methodology.txt And the final element of the first extension part is render. This is the template class processor. We did not make it general to provide with enough freedom for realizing within extension. It is worth mentioning that usually this class Render is located in the root extension folder. In this case the template files should be placed in the directory view on the same level.

Image 2

On practice all the three elements organize the chain of the sequential actions as on the image 2.

First step

We suggest you to realize the following idea: the additional field on the customer editing page for backend. To make it simple let it be the text field. An administrator will be able to fill in a word for defining a customer, for example a rank. It is enough for us at the moment.

Thus, let us start working on this task. We assume that our new field will be called Rank. The whole task comes down to adding some classification of these ranks, let us call our extension Customers rank.

After reviewing the system architecture you should be familiar with the start point for any extension. This is the folder /lib/common/extensions/ where all the extensions regardless of their purpose are located. Let us define the place for our new extension and create the new folder. It should be taken into account that the words beginning with the capital letter and without spaces in the title, so called CamelCase. Then we create the folder CustomersRank. Within this folder we create the file CustomersRank.php which content will look in the following way:

<?php

namespace common\extensions\CustomersRank;

class CustomersRank extends \common\classes\modules\ModuleExtensions {

}

It is important to remember that the folder title, the main file title and the class title will always be the same, the letter case should always be taken into account. In our file namespace indicates the extension location, that is the path to the folder in fact. Also it is necessary to pay attention to the parent class that we inherit. For all the extensions it will be the same and will have the set of ready-made instructions for working with extension. On practice, it means that at this stage your extension is ready for installation already and you will be able to see it in the list of the uninstalled extensions:

Image 3

You can install extension and switch it on. But since we did not want our extension to do we get the corresponding result. Let us add some more content to our main file. In order to do it we will need the elements from the section basic functionality. First of all, functions. It is the usual static method located in the main class. We indeed start from this element since its performance can be checked directly via controller, despite that in the future query will be managed by the second element from our set - hooks. Let us add render function of the additional customer field to our class:

    public static function renderCustomerField() 
    {
        return 'I want render new field';
    }

Now in order to see the returned text let us use the direct query from the controller backend:

extensions?module=CustomersRank&action=renderCustomerField Depending on if you switch extension on or off the result will differ:

You have not rights for this extension: CustomersRank

or

I want render new field

It is important to remember that such method is not safe and is available for the admin area only. It should always be taken into account and perform the necessary checks within function. The next step as it was promised will be the connection of extension to the customer editing page. The list of the possible values can be seen in the file lib/common/extensions/methodology.txt in the corresponding section Hooks. Let us see into what kind of list it is and how to understand what place should be exactly used. Open any customer editing page in backend and pay attention to page url

admin/customers/customeredit?customers_id=XXX

And now let us pay attention to the query list. The one that interests us will be similar to the page url. Let us select the ones that meet our requirement:

'customers/customeredit', ''
'customers/customeredit/before-render', ''
'customers/customeredit', 'personal-block'
'customers/customeredit', 'left-column'
'customers/customeredit', 'right-column'

We understood the first key part but how to understand what the second key is? It is quite simple! If the second parameter is absent then this is the controller in the post moment. We will use it for saving data in the future. The other four values indicate the location on the page. Try all of them necessarily, but in our example we try personal-block that corresponds to the block with the name Personal Details. So we know whick key to use, let us understand how to use it. All the query points are described in the function getAdminHooks as array. Add the new function to your main class:

public static function getAdminHooks() 
    {
        $path = \Yii::getAlias('@common') . DIRECTORY_SEPARATOR . 'extensions' . DIRECTORY_SEPARATOR . 'CustomersRank' . DIRECTORY_SEPARATOR . 'hooks' . DIRECTORY_SEPARATOR;
        return [
            [
                'sort_order' => 10,
                'page_name' => 'customers/customeredit',
                'page_area' => 'personal-block',
                'extension_file' => $path . 'customers.customeredit.personal-block.tpl',
            ],
        ];
    }

Data about the interception point is transferred as array. We did not choose the file title randomly, but combine the page and block titles. The file extension indicates that it is the template and Smarty is used for processing these files. You can find more details regarding syntax and function in Smarty official documentation. It will allow to avoid confusion in the future when there are a lot of similar interceptions. The subcategory hooks is usually chosen as the place for keeping our interception files. Let us create the directory and the file in it. The file content will look in the following way:

{if $ext = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')}
    {$ext::renderCustomerField()}
{/if}

We did everything correctly, but after we refresh the page we do not see our text. It happens because hooks are attributed during the installation process and we will have to clear them manually or reinstall extension. Fortunately, clearing cache can be done easily and you will use this page quite often during the development process since a lot of data is cached by system. For example, system cache caches кэширует constants and even the table structure, smarty caches precompiled templates, opcache caches php files. Now we are interested in clearing cache for hooks:

Image 4

We click on Flush, wait for the popup with the message about the successful clearing and return to the customer editing page. In the lower blog part we will see out text:

Image 5

But we have not reached our goal yet. We will have to use the third element from the render section. It is also a very important extension part that allows to use the smarty templates for content output. Unlike function it is the separate class, usually we place it on the same level with the main class. Create the file Render.php in the root extension folder.

<?php

namespace common\extensions\CustomersRank;

class Render extends \common\classes\extended\Widget {

    public $params;
    public $template;

    public function run() {
        return $this->render($this->template, $this->params);
    }
}

Since the file and the main file are located in the same place, our namespace remains the same. We will inherit it from the widget, announce a couple of parameters that we are going to render and the simple function of the content render. Also create the folder with the title views in the same place where the file Render.php is located. In this folder we will make our first template. Create the file with the following content:

{use class="\yii\helpers\Html"}
<div class="w-line-row w-line-row-1">
    <div class="wl-td">
        <label>Rank</label>
        {Html::input('text', 'rank', $rank, ['class' => 'form-control'])}
    </div>
</div>

Beside the usual html we will use framework function yii. It is possible to get familiarized in more detail about the work with the fields in yii in the official documentation. And finally the last action is the render query instead of returning text in our function renderCustomerField. Let us change it to the following:

public static function renderCustomerField() 
    {
        $rank = 'I want render new field';
        return \common\extensions\CustomersRank\Render::widget(['template' => 'customer-field', 'params' => ['rank' => $rank]]);
    }

If you use php files caching, it is possible you will need to clear OPcache as it was described above:

Image 6

You can understand why we use these or those classes inspecting the prepared form element:

Image 7

Let us sum up. At the moment our extension cannot save data. We will explain how to do it in the future. But even now we can create the main class defined by system as extension and allowing to perform installation/deletion. Also we used all the elements of basic functionality section that are static methods, interception, render. If something did not work from your end, you can download the example with the result that should be reached here.

Look at tool

Let us come back to the image 1. Before we reviewed basic functionality, but it was not enough to finish extension completely.

Today we will get familiarized with the major part of Tools section. Its main role is the data manipulation. First of all, this is models. It is the object-oriented interface for accessing and managing data kept in the databases. Since it is the prepared work mechanism with data Active Record in yii, we will inherit our models from them. We will use the eponymous subcategory models for keeping models.

One more element is helpers. These are the classes containing the query static methods with frequently used code fragments. Using helpers we stick to the principle of reusing code so it allows to avoid redundancy. We think you have already guessed that the directory for this type objects will be called helpers.

Unlike helpers the element classes is the classic objects for object oriented programming. The goal of our lectures is not to understand how to use objects in programming. Regarding location in architecture it is also rather straightforward – this is the classes directory.

More specific element is widgets. They are multi reusable building blocks used in representations for creating complicated and customizable elements of user interface by means of the built-in theme editor. From all we previously reviewed widgets are similar to render elements, but unlike them widgets act in another way.

Finish simple extension

We previously created incomplete extension that cannot save data yet. Now we will create the table and learn to use our own models. Models are our auxiliary elements from Tools section (see image 1) that are placed in the subfolder models as a rule. To divide tables according to the logic principle we recommend to use the table prefix. In our example we will use the table ranks. We will call the model file as Ranks.php and place it in the models subdirectory. And the table will get the prefix cr_- it will mean Customer Ranks. It is convenient if your extension uses a few tables, besides for sure you avoid the possibility of crossing with the strange tables called rank. As a result, our model will look as follows:

<?php

namespace common\extensions\CustomersRank\models;

use yii\db\ActiveRecord;

class Ranks extends ActiveRecord
{
    public static function tableName()
    {
        return 'cr_ranks';
    }
    
}

Namespace, that we previously reviewed, includes the directory models.  Inheritance from the basic active record is used and the function tableName, allowing the model to identify itself, is added. It is enough for the model to start working. It is possible to get familiarized with all the model possibilities in the framework documentation by visiting this link.

We will find out later how to make extension create a table when we review the section Setup. Now we confine ourselves to adding the table to the database by means of SQL:

CREATE TABLE IF NOT EXISTS `cr_ranks` (
  `customers_id` int(11) NOT NULL,
  `customer_rank` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`customers_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Now we have the table and the model. We just need to save data. In order to do it we will use the knowledge from the previous lesson and create one more function saveCustomerField where we will address to the model:

public static function saveCustomerField($id) 
    {
        $rank = filter_var( \Yii::$app->request->post('rank', ''), FILTER_SANITIZE_STRING);
        $ranksRecord = \common\extensions\CustomersRank\models\Ranks::find()
                ->where(['customers_id' => $id])
                ->one();
        if (!($ranksRecord instanceof \common\extensions\CustomersRank\models\Ranks)) {
            $ranksRecord = new \common\extensions\CustomersRank\models\Ranks();
            $ranksRecord->customers_id = $id;
        }
        $ranksRecord->customer_rank = $rank;
        $ranksRecord->save();
    }

Also we will need the new hook looking not at the block, but at the controller during form data saving. We will call the file itself as customers.customeredit.php and it will look in the following way:

<?php
if ($CustomersRank = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')) {
  $CustomersRank::saveCustomerField((int) $cInfo->customers_id);
}

It is important not to forget to make the changes in the function getAdminHooks. Now it will contain two elements. We will give the content of the updated function:

public static function getAdminHooks() 
    {
        $path = \Yii::getAlias('@common') . DIRECTORY_SEPARATOR . 'extensions' . DIRECTORY_SEPARATOR . 'CustomersRank' . DIRECTORY_SEPARATOR . 'hooks' . DIRECTORY_SEPARATOR;
        return [
            [
                'sort_order' => 10,
                'page_name' => 'customers/customeredit',
                'page_area' => 'personal-block',
                'extension_file' => $path . 'customers.customeredit.personal-block.tpl',
            ],
            [
                'sort_order' => 20,
                'page_name' => 'customers/customeredit',
                'page_area' => '',
                'extension_file' => $path . 'customers.customeredit.php',
            ],
        ];
    }

As you can see from the new hook code we used the parameter for rendering the object identifier edited by you. If necessary we could render the whole customer object, but in this case there is no need to do it. Now let us clear opcache and hooks as we did it before and try to save the word sheriff In our new field. We did everything correctly but no result is seen. And nonetheless, if the database is checked you will find the record in the new table. The reason for it is that we do not extract data from the old function renderCustomerField. Before we do it, let us create the helper that returns the customer rank. We create the directory helpers and the file Customer.php in it with the following content:

<?php

namespace common\extensions\CustomersRank\helpers;

class Customer {

    public static function getRank($id) {
       $ranksRecord = \common\extensions\CustomersRank\models\Ranks::find()
                ->where(['customers_id' => $id])
                ->one();
       if ($ranksRecord instanceof \common\extensions\CustomersRank\models\Ranks) {
           return $ranksRecord->customer_rank;
       }
        return '';
    }
       
}

We see previously reviewed namespace and class containing the static method. Now we can query this helper every time we need to get the customer rank. Let us add the query to our function:

public static function renderCustomerField($id = 0) 
    {
        $rank = \common\extensions\CustomersRank\helpers\Customer::getRank($id);
        return \common\extensions\CustomersRank\Render::widget(['template' => 'customer-field', 'params' => ['rank' => $rank]]);
    }

It is important not to forget that our function requires parameter now. Though we used value by default to avoid a mistake, for complete work we will need to change the old file customers.customeredit.personal-block.tpl in such a way, that the function will get the customer identifier. The procedure is similar to one we did during saving:

{if $ext = \common\helpers\Acl::checkExtensionAllowed('CustomersRank', 'allowed')}
    {$ext::renderCustomerField($cInfo->customers_id)}
{/if}

Let us clear caches and try to refresh the page. We hope you will get the following result:

Image 8

Thus the field is saved. Our administrators are happy until the users start asking what rank is assigned to them. There is one more unused tool - widgets. And this tool indeed will help to resolve the more complicated task. We have already mentioned that widget is very similar to render, thus it will be the separate class and the separate template. Let us create the category widgets in our extension folder. Now let us come up with our widget title. Its task is to display the rank assigned by a administrator, therefore we call the widget ShowRank. We create the eponymous subcategory and the new file in this directory. As you already guessed the file title is ShowRank.php and it will look as follows:

<?php

namespace common\extensions\CustomersRank\widgets\ShowRank;

class ShowRank extends \yii\base\Widget
{
    public $name;
    public $params;
    public $settings;

    public function run()
    {
        if (\Yii::$app->user->isGuest) {
            return '';
        }
        $customer = \Yii::$app->user->getIdentity();
        $rank = \common\extensions\CustomersRank\helpers\Customer::getRank($customer->customers_id);
        return self::begin()->render('customer-field', [
            'rank' => $rank,
        ]);
    }
}

First of all we forbid the widget work for not logged in customers since there is nothing to display to them. Then we get the data about the current user and used his identifier for getting rank with the helper we wrote before. And we render value to the template that does not exist yet. Before we create the template, on our widget level we create subdirectory views, and there we create the simple template customer-field.tpl that will output the text: