Creating Extension from Scratch: Difference between revisions

From osCommerce Wiki
Jump to navigation Jump to search
(13 intermediate revisions by the same user not shown)
Line 39: Line 39:
[[File:Image 828.png|none|frame|''Image 3'']]
[[File:Image 828.png|none|frame|''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:
You can install extension and switch it on. But since we did not want our extension to do anything 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:
<pre>
<pre>
     public static function renderCustomerField()  
     public static function renderCustomerField()  
Line 45: Line 45:
         return 'I want render new field';
         return 'I want render new field';
     }
     }
</pre>Now in order to see the returned text let us use the direct query from the controller ''backend'':  
</pre>Now in order to see the returned text let us use the direct query from the controller ''backend'' via the browser url:  


''extensions?module=CustomersRank&action=renderCustomerField''
''extensions?module=CustomersRank&action=renderCustomerField''
It means that if your shop url is <nowiki>http://localhost/oscommerce/</nowiki>, then your backend url is <nowiki>http://localhost/oscommerce/admin/</nowiki> and the direct query from the controller ''backend'' via the browser url is <nowiki>http://localhost/oscommerce/admin/</nowiki>''extensions?module=CustomersRank&action=renderCustomerField''   
Depending on if you switch extension on or off the result will differ:
Depending on if you switch extension on or off the result will differ:


Line 56: Line 59:
''<code>I want render new field</code>''
''<code>I want render new field</code>''


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.  
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
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


Line 87: Line 91:
     {$ext::renderCustomerField()}
     {$ext::renderCustomerField()}
{/if}
{/if}
</pre>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'':
</pre>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'':
[[File:Image 827.png|none|frame|''Image 4'']]
[[File:Image 827.png|none|frame|''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:
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 block part we will see our text:
[[File:Image 831.png|none|frame|''Image 5'']]
[[File:Image 831.png|none|frame|''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.
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.
Line 223: Line 227:
[[File:Image 834.png|none|frame|''Image 8'']]
[[File:Image 834.png|none|frame|''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.
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:
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:
<pre><?php
<pre><?php
Line 245: Line 250:
         ]);
         ]);
     }
     }
}</pre>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:
}</pre>First of all we forbid the widget work for not logged in customers since there is nothing to display to them. Then we got 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:
<pre><p>You rank: {$rank}</p></pre>The widget is practically prepared, we just need to inform the system about its existence. In order to do it we need to return to the main class and add one more function:
<pre><p>You rank: {$rank}</p></pre>The widget is practically prepared, we just need to inform the system about its existence. In order to do it we need to return to the main class and add one more function:
<pre>public static function getWidgets($type = 'general') {
<pre>public static function getWidgets($type = 'general') {
Line 264: Line 269:
The users do not see our changes yet. In order to confirm them click on the orange button ''Save''. Now you will be able to see the same result in the user account that you previously logged in.  
The users do not see our changes yet. In order to confirm them click on the orange button ''Save''. Now you will be able to see the same result in the user account that you previously logged in.  


Let us sum up: now our extension is capable not to save data only, but also represent them to a user. We managed to do it due to the new tools - '''model''', '''widget''' and also '''helper''' helped us a little. We hope their usage did not cause any difficulties for you. If you failed to do something on your own you can download the example with the result from this [https://www.oscommerce.com/lessons/extensions.lesson2.zip link].
Let us sum up: now our extension is capable not only to save data, but also represent it to a user. We managed to do it due to the new tools - '''model''', '''widget''' and also '''helper''' helped us a little. We hope their usage did not cause any difficulties for you. If you failed to do something on your own you can download the example with the result from this [https://www.oscommerce.com/lessons/extensions.lesson2.zip link].


== What is Bootstrap ==
== What is Bootstrap ==
Line 294: Line 299:
     }
     }


}</pre>As you can see our controller is intended for backend only. It does not exist at the present moment so let us create the directories ''backend'' and ''controllers'' in the same way, it was indicated in bootstrap. Now there is the place where we can locate our first controller. As always, the file title ''ManageRankController.php'' is identical with the class title. In order to check if bootstrap is set up correctly, the  class with one function will be enough:
}</pre>As you can see our controller is intended for backend only. It does not exist at the present moment so let us create the directories ''backend'' and ''controllers'' in the same way as it was indicated in bootstrap. Now there is the place where we can locate our first controller. As always, the file title ''ManageRankController.php'' is identical with the class title. In order to check if bootstrap is set up correctly, the class with one function will be enough:
<pre><?php
<pre><?php


Line 462: Line 467:


== Setup need or not ==
== Setup need or not ==
  Now we already have the complete extension. Before we start sharing it we should touch the issue of its installation and deletion from the system. That is why there is the last extension '''setup''' section (see image 1). '''Tables''' creation, if tables should be deleted during de-installation, '''translations''' creation for different languages, how to add a new element to '''menu,''' access differentiation for administrators by means of '''acl''' and other useful little things that are required for the full automation will be applied in the next chapter on practice.  
Now we already have the complete extension. Before we start sharing it we should touch the issue of its installation and deletion from the system. That is why there is the last extension '''setup''' section (see image 1). '''Tables''' creation, if tables should be deleted during de-installation, '''translations''' creation for different languages, how to add a new element to '''menu,''' access differentiation for administrators by means of '''acl''' and other useful little things that are required for the full automation will be applied in the next chapter on practice.  


== Let's finish extension ==
== Let's finish extension ==
Line 475: Line 480:
     {
     {
         return [
         return [
             '1.0.0' => ['whats_new' => "Customers Rank аirst version"],
             '1.0.0' => ['whats_new' => "Customers Rank first version"],
         ];
         ];
     }
     }
Line 484: Line 489:
     }
     }
}</pre>As you can see at the moment the file contains the brief description and the version of your extension only. Now we render the method ''getAdminHooks'' from the main class. And we work on creating and deleting the table. Two methods will help us in it:
}</pre>As you can see at the moment the file contains the brief description and the version of your extension only. Now we render the method ''getAdminHooks'' from the main class. And we work on creating and deleting the table. Two methods will help us in it:
<pre>public static function install($platform_id, $migrate)
    {
        $migrate->createTableIfNotExists('cr_ranks', [
            'customers_id' => $migrate->primaryKey(),
            'customer_rank' => $migrate->string(32)
        ]);
    }
   
    public static function getDropDatabasesArray()
    {
        return ['cr_ranks'];
    }</pre>To create tables we use the prepared class \common\classes\Migration, you can review all its possibilities on your own. And for deleting it will be enough to return the table array that can be deleted.
Now let us work on translations. For it we will need to create the method getTranslationArray, that will return the array of arrays. In order to understand what it is all about we recommend to see the translation page in backend. The first key is entity, the second one is the constant. There are public entities such as main and admin/main, we use it for the future menu element. The menu translation should be always available regardless we went to our page or not, therefore we use admin/main. We will need the other constants locally and we use them in the backend controller.
<pre>public static function getTranslationArray() {
        return [
            'admin/main' => [
                'BOX_CUSTOMERS_RANK' => 'Customers Rank',
            ],
            'extensions/customers-rank' => [
                'CR_NAME' => 'Customer name',
                'CR_RANK' => 'Rank',
                'CR_ACTION' => 'Action'
            ],
        ];
    }</pre>And since we added translation for menu let us create the element itself and the visibility rules:
<pre>public static function getAdminMenu()
    {
        return [
            [
                'parent' => 'BOX_HEADING_CUSTOMERS',
                'sort_order' => '100',
                'acl_check' => 'CustomersRank,allowed',
                'path' => 'manage-rank',
                'title' => 'BOX_CUSTOMERS_RANK',
            ],
        ];
    } 
   
    public static function getAclArray()
    {
        return ['default' => ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK']];
    }</pre>Now let us adjust our controller a little by adding binding to the new menu element and replacing texts to constants:
<pre>public $acl = ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK'];
   
    public function actionIndex()
    {
        $this->navigation[] = array('link' => \Yii::$app->urlManager->createUrl('manage-rank/'), 'title' => 'Customers Rank');
        $table = [
            [
                'title' => CR_NAME,
                'not_important' => 0,
            ],
            [
                'title' => CR_RANK,
                'not_important' => 0,
            ],
            [
                'title' => CR_ACTION,
                'not_important' => 0,
            ],
        ];
        return $this->render('index', [
            'table' => $table,
        ]);
    }</pre>Thus we finished creating the fully functional extension. Now you can quietly delete and install your extension via backend. If you are not sure about your code correctness or something does not work as it should you can download the lesson [https://www.oscommerce.com/lessons/extensions.lesson4.zip here].
== Make my life easy ==
It is very important to be able to create extension from zero, but we understand that to perform the monotonous actions for creating the same directories and files convert programing in routine. Therefore, during the planning stage, we allow to use the generator, that you can get familiarized with [https://generator.tllab.co.uk/ here].
The template generation is done in three stages. The first stage is the input of the general information:
[[File:Image 838.png|none|frame]]
We have already done it manually, creating folders and classes, filling in versioning and description.
The second stage allows to perform more accurate setting of the used elements that will also not become the surprise for you:
[[File:Image 839.png|none|frame]]
And the third stage allows to download the prepared archive, having performed a little tweaking:
[[File:Image 840.png|none|frame]]
Try to create the template analogue for the extension that was manually created and compare the results on your own. Which of the results will you like more?
== Epilogue ==
Let us finish our review. We hope you did not waste your time and you will be able to create extensions on your own, that are far more sophisticated in functionality than the Customer Rank extension. Our team is also improving the system and maybe, in the near future there will be new features that we will certainly let you know about. We are glad you stayed with us all this time.
'''''Created by the developer, Yuriy Nechitajlo'''''

Revision as of 15:58, 21 September 2022

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 anything 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 via the browser url:

extensions?module=CustomersRank&action=renderCustomerField

It means that if your shop url is http://localhost/oscommerce/, then your backend url is http://localhost/oscommerce/admin/ and the direct query from the controller backend via the browser url is http://localhost/oscommerce/admin/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 block part we will see our 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 got 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:

<p>You rank: {$rank}</p>

The widget is practically prepared, we just need to inform the system about its existence. In order to do it we need to return to the main class and add one more function:

public static function getWidgets($type = 'general') {
        if ( !self::allowed() ) return [];

        $widgets = [];
        if ($type == 'account') {
            $widgets[] = ['name' => 'CustomersRank\widgets\ShowRank', 'title' => 'Show Rank', 'description' => '', 'type' => 'account', 'class' => ''];
        }
        return $widgets;
    }

It will allow a designer to understand that we provide with the widget for the customer account page. Let us clear cache and get familiarized with the internal theme designer.

Before we open the theme designer login as a user on the frontend. Without the active user all we could see is the login page. Now let us choose from the menu Design and CMS -> Themes, click on the button Customize -> Desktop on the theme we are going to change. We see the index page. In order to change the page we choose Pages in the right menu, the category Account and the page Account. Most likely you will see the page similar to the following one:

Image 9

Let us not create the separate blocks, but simply click on Add Widgets in the existing block (see image 9). In the popup window you will see the full list of the available widgets. Let us use the search tool:

Image 10

We find our widget and just click on it. The system will automatically add it in front of the block and in the editer we will be able to see the result:

Image 11

The users do not see our changes yet. In order to confirm them click on the orange button Save. Now you will be able to see the same result in the user account that you previously logged in.

Let us sum up: now our extension is capable not only to save data, but also represent it to a user. We managed to do it due to the new tools - model, widget and also helper helped us a little. We hope their usage did not cause any difficulties for you. If you failed to do something on your own you can download the example with the result from this link.

What is Bootstrap

We have already reviewed two sections of the image 1, it is time to pay attention to one more section. In fact, Bootstrap is the mechanism of the initial downloading before the extension runs and the incoming query is processed. It is this feature that allows us to integrate our own controllers in the system, wherein view is more the consequence of our using controllers than the reason. It is important to understand, that bootstrap is public for backend, frontend and even for console. Therefore we should limit the visibility area ourselves. You will find out how to do it in the next chapter.

Improve my extension

It is time to complicate the task. It is not an easy task to look through each customer to see what rank he has. It is time to organize the separate list and add the possibility of changing the rank value additionally.

As you already guessed we use bootstrap. To create preload in Yii2, usually we create the eponymous file Bootstrap.php with the class, that should perform interface BootstrapInterface and its method bootstrap. Let us create this file in the root extension folder, its content will look in the following way:

<?php

namespace common\extensions\CustomersRank;

use yii\base\BootstrapInterface;

class Bootstrap implements BootstrapInterface {

    public function bootstrap($app) {
        if (!CustomersRank::enabled()) {
            return;
        }
        if ($app instanceof \yii\web\Application) {
            if ($app->id == 'app-backend') {
                $app->controllerMap = array_merge($app->controllerMap, [
                    'manage-rank' => ['class' => __NAMESPACE__ . '\backend\controllers\ManageRankController'],
                ]);
            }
        }
    }

}

As you can see our controller is intended for backend only. It does not exist at the present moment so let us create the directories backend and controllers in the same way as it was indicated in bootstrap. Now there is the place where we can locate our first controller. As always, the file title ManageRankController.php is identical with the class title. In order to check if bootstrap is set up correctly, the class with one function will be enough:

<?php

namespace common\extensions\CustomersRank\backend\controllers;

class ManageRankController extends \common\classes\modules\SceletonExtensionsBackend {

    public $acl = ['BOX_HEADING_CUSTOMERS'];
    
    public function actionIndex() {
        die('my controller');
    }
}

We are already quite familiar with the notion namespace and it exactly repeats the path to the file. We inherited the class itself from SceletonExtensionsBackend to switch on the automatic check regarding ownership. Therefore, in the variable $acl it is necessary to indicate what menu element we relate to. Since we do not have our own element yet, let us inform the system that it is Customers. The function actionIndex is the event performed by default. It means that the query to url manage-rank or manage-rank/index will be identical, but the first one is more user friendly. Try both ways of querying to the page (it is worth remembering that now we work with backend so your url should be similar to https://localhost/admin/manage-rank/index).

Now let us convert our page to list. In order to do it, it will be necessary to perform a number of actions. Let us start with actionIndex change. Despite that we do not have the menu element we still should indicate where we are via navigation. It will be displayed in the page title. Also we will prepare the array with the data about our future table title and render it to the template:

public function actionIndex() 
    {
        $this->navigation[] = array('link' => \Yii::$app->urlManager->createUrl('manage-rank/'), 'title' => 'Customers Rank');
        $table = [
            [
                'title' => 'Customer name',
                'not_important' => 0,
            ],
            [
                'title' => 'Rank',
                'not_important' => 0,
            ],
            [
                'title' => 'Action',
                'not_important' => 0,
            ],
        ];
        return $this->render('index', [
            'table' => $table,
        ]);
    }

Now let us create the template that does not exist yet. The path to the file in regard to the root extension folder will be backend/views/manage-rank/index.tpl. As you can see, everything that relates to backend is located in one directory and in fact repeats the work of the system as the whole. Below we demonstrate the file content that includes the title, the function table on javascript, that we are going to use when the button in the column Action is clicked:

{use class="common\helpers\Html"}
<div class="page-header">
    <div class="page-title">
        <h3>{$app->controller->view->headingTitle}</h3>
    </div>
</div>
<div class="rank-wrap">
    <table class="table table-bordered table-hover table-responsive table-checkable datatable" checkable_list="0, 1" data_ajax="manage-rank/list">
        <thead>
            <tr>
                {foreach $table as $tableItem}
                    <th{if isset($tableItem['not_important']) && $tableItem['not_important'] == 1} class="hidden-xs"{/if}>{$tableItem['title']}</th>
                {/foreach}
            </tr>
        </thead>
    </table> 
</div>
<script type="text/javascript">
function newCustomerRank(id) {
    bootbox.dialog({
        message: '<div class="new-rank">Rank: {Html::input('text', 'rank', '', ['class' => 'form-control'])|escape:javascript}</div>',
        title: "New rank",
        buttons: {
            done:{
                label: "{$smarty.const.TEXT_BTN_OK}",
                className: "btn-cancel",
                callback: function() {
                    var rank = $('input[name="rank"]').val();
                    $.get("manage-rank/change", { 
                        'id': id,
                        'rank': rank
                    }, function () {
                        var table = $('.table').DataTable();
                        table.draw(false);
                    }, "html");
                }
            }
        }
    });
    
    
    return false;
}
</script>

As you can see we are going to use two more additional actions in the controller. The first one is required for the table:

public function actionList() 
    {
        \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
        
        $draw = \Yii::$app->request->get('draw', 1);
        $start = \Yii::$app->request->get('start', 0);
        $length = \Yii::$app->request->get('length', 10);
        if ($length == -1) {
            $length = 10000;
        }
        
        
        
        $customersQuery = \common\models\Customers::find()
                ->select(['c.customers_id', 'c.customers_firstname', 'c.customers_lastname', 'r.customer_rank'])
                ->from(\common\models\Customers::tableName() . ' c')
                ->leftJoin(\common\extensions\CustomersRank\models\Ranks::tableName() . ' r', 'c.customers_id=r.customers_id')
                ;
        $search = \Yii::$app->request->get('search');
        if (isset($search['value']) && tep_not_null($search['value'])) {
            $keywords = tep_db_input(filter_var($search['value'], FILTER_SANITIZE_STRING));
            $customersQuery->andWhere("c.customers_firstname like '%" . $keywords . "%' or c.customers_lastname like '%" . $keywords . "%'");
        }
        
        $order = \Yii::$app->request->get('order');
        if (isset($order[0]['column']) && $order[0]['dir']) {
            switch ($order[0]['column']) {
                case 0:
                    $customersQuery->orderBy("c.customers_firstname " . tep_db_input(tep_db_prepare_input($order[0]['dir'])));
                    break;
                case 1:
                    $customersQuery->orderBy("r.customer_rank " . tep_db_input(tep_db_prepare_input($order[0]['dir'])));
                    break;
                default:
                    $customersQuery->orderBy("c.customers_firstname, c.customers_lastname");
                    break;
            }
        } else {
            $customersQuery->orderBy('c.customers_firstname, c.customers_lastname');
        }
        
        $responseList = [];
        $rowsShow = 0;
        $rowsTotal = $customersQuery->count();
        $customersQuery->limit($length)->offset($start);
        
        foreach ($customersQuery->asArray()->all() as $customersRow) {
            $rowsShow++;
            $responseList[] = [
                $customersRow['customers_firstname'] . ' ' . $customersRow['customers_lastname'],
                $customersRow['customer_rank'],
                '<a class="btn" href="javascript:void(0);" onclick="return newCustomerRank(' . $customersRow['customers_id'] . ')">Change rank</a>'
            ];
        }
        
        $response = [
            'draw' => $draw,
            'recordsTotal' => $rowsTotal,
            'recordsFiltered' => $rowsShow,
            'data' => $responseList
        ];
        return $response;
    }

And the second one will be required for changing our field value:

public function actionChange() 
    {
        $id = (int)\Yii::$app->request->get('id');
        $rank = filter_var(htmlentities(\Yii::$app->request->get('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->customer_rank = $rank;
           $ranksRecord->save();
       } else {
           $customer = \common\models\Customers::find()
                   ->where(['customers_id' => $id])
                   ->one();
           if ($customer instanceof \common\models\Customers) {
            $ranksRecord = new \common\extensions\CustomersRank\models\Ranks();
            $ranksRecord->customers_id = $id;
            $ranksRecord->customer_rank = $rank;
            $ranksRecord->save();
           }
       }
    }

Now all the parts are gathered together. You only need to clear cache and check your page performance. Hopefully your code came out to be workable otherwise you can download the lesson example here.

Setup need or not

Now we already have the complete extension. Before we start sharing it we should touch the issue of its installation and deletion from the system. That is why there is the last extension setup section (see image 1). Tables creation, if tables should be deleted during de-installation, translations creation for different languages, how to add a new element to menu, access differentiation for administrators by means of acl and other useful little things that are required for the full automation will be applied in the next chapter on practice.

Let's finish extension

Extension is almost ready, we only need to polish little things. For example, to create our own menu element, to automate the table addition, render texts to translation. Let us start with adding the file Setup.php in your extension root folder.

<?php

namespace common\extensions\CustomersRank;

class Setup extends \common\classes\modules\SetupExtensions {

    public static function getVersionHistory() 
    {
        return [
            '1.0.0' => ['whats_new' => "Customers Rank first version"],
        ];
    }

    public static function getDescription()
    {
        return 'This extension allows you to assign ranks to customers';
    }
}

As you can see at the moment the file contains the brief description and the version of your extension only. Now we render the method getAdminHooks from the main class. And we work on creating and deleting the table. Two methods will help us in it:

public static function install($platform_id, $migrate)
    {
        $migrate->createTableIfNotExists('cr_ranks', [
            'customers_id' => $migrate->primaryKey(),
            'customer_rank' => $migrate->string(32)
        ]);
    }
    
    public static function getDropDatabasesArray()
    {
        return ['cr_ranks'];
    }

To create tables we use the prepared class \common\classes\Migration, you can review all its possibilities on your own. And for deleting it will be enough to return the table array that can be deleted.

Now let us work on translations. For it we will need to create the method getTranslationArray, that will return the array of arrays. In order to understand what it is all about we recommend to see the translation page in backend. The first key is entity, the second one is the constant. There are public entities such as main and admin/main, we use it for the future menu element. The menu translation should be always available regardless we went to our page or not, therefore we use admin/main. We will need the other constants locally and we use them in the backend controller.

public static function getTranslationArray() {
        return [
            'admin/main' => [
                'BOX_CUSTOMERS_RANK' => 'Customers Rank',
            ],
            'extensions/customers-rank' => [
                'CR_NAME' => 'Customer name',
                'CR_RANK' => 'Rank',
                'CR_ACTION' => 'Action'
            ],
        ];
    }

And since we added translation for menu let us create the element itself and the visibility rules:

public static function getAdminMenu()
    {
        return [
            [
                'parent' => 'BOX_HEADING_CUSTOMERS',
                'sort_order' => '100',
                'acl_check' => 'CustomersRank,allowed',
                'path' => 'manage-rank',
                'title' => 'BOX_CUSTOMERS_RANK',
            ],
        ];
    }   
    
    public static function getAclArray()
    {
        return ['default' => ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK']];
    }

Now let us adjust our controller a little by adding binding to the new menu element and replacing texts to constants:

public $acl = ['BOX_HEADING_CUSTOMERS', 'BOX_CUSTOMERS_RANK'];
    
    public function actionIndex() 
    {
        $this->navigation[] = array('link' => \Yii::$app->urlManager->createUrl('manage-rank/'), 'title' => 'Customers Rank');
        $table = [
            [
                'title' => CR_NAME,
                'not_important' => 0,
            ],
            [
                'title' => CR_RANK,
                'not_important' => 0,
            ],
            [
                'title' => CR_ACTION,
                'not_important' => 0,
            ],
        ];
        return $this->render('index', [
            'table' => $table,
        ]);
    }

Thus we finished creating the fully functional extension. Now you can quietly delete and install your extension via backend. If you are not sure about your code correctness or something does not work as it should you can download the lesson here.

Make my life easy

It is very important to be able to create extension from zero, but we understand that to perform the monotonous actions for creating the same directories and files convert programing in routine. Therefore, during the planning stage, we allow to use the generator, that you can get familiarized with here.

The template generation is done in three stages. The first stage is the input of the general information:

Image 838.png

We have already done it manually, creating folders and classes, filling in versioning and description.

The second stage allows to perform more accurate setting of the used elements that will also not become the surprise for you:

Image 839.png

And the third stage allows to download the prepared archive, having performed a little tweaking:

Image 840.png

Try to create the template analogue for the extension that was manually created and compare the results on your own. Which of the results will you like more?

Epilogue

Let us finish our review. We hope you did not waste your time and you will be able to create extensions on your own, that are far more sophisticated in functionality than the Customer Rank extension. Our team is also improving the system and maybe, in the near future there will be new features that we will certainly let you know about. We are glad you stayed with us all this time.

Created by the developer, Yuriy Nechitajlo