Varnish: the secret weapon for boosting page speed

Before We Begin

Request processing (especially dynamic) with web servers such as Apache can be a heavy strain on system resources.

That’s why more and more people started using lightweight web servers like nginx or lighttpd; they can work as a frontend for static content paired with Apache or independently. But there is another way to improve your site: using a web server accelerator such as Varnish Cache – a solution from Varnish Software.

There are a few ways to learn all the specifics of Varnish Cache processing: reading the Varnish Book, the Official documentation and a lot of practice and experimentation. We highly recommend starting with the first two. This article is based on all three, materials found at stackoverflow and other professional sites, and personal experience.

 

What is Varnish?

VARNISH is an open source caching reverse HTTP proxy, sometimes referred to as a web application accelerator. A reverse proxy is a proxy server that appears to clients as an ordinary server. It stores (caches) files or fragments of files in memory that are used to reduce the response time and network bandwidth consumption.

Varnish is available under a two-clause BSD licence. Commercial support is available from Varnish Software. Version 1.0 of Varnish was released in 2006, Varnish 2.0 in 2008, Varnish 3.0 in 2011 and Varnish 4.0 in 2014.

Varnish is flexible because you can configure it by writing your own caching policies in its Varnish Configuration Language (VCL). VCL is a domain specific language based on C. VCL is then translated to C code and compiled.

 

How does Varnish work?

Let’s imagine that the client sends a request to your server. Varnish receives it and tries to find an appropriate response in the cache storage. If it finds one and its content is still valid, then we have a response we can deliver to the client. Otherwise Varnish will forward the request to the backend, fetch the response, store it in the cache and send the response to the client. Next time the same request is made, Varnish will have the response in cache storage.

It’s easy to represent this with a simple diagram:

varnish cache flowchart

 

Installation and basic configuration of Varnish

 

You can easily find Varnish binary packages or compile it for yourself from source code. Use the official site for grabbing a current version. Installation can be done with the system package manager, but please note that this might not be the latest version of Varnish. For example under Debian 8 (“jessie”) do the following as root:

[code]

apt-get install apt-transport-https
curl https://repo.varnish-cache.org/GPG-key.txt | apt-key add -
echo "deb https://repo.varnish-cache.org/debian/ jessie varnish-4.1" >> /etc/apt/sources.list.d/varnish-cache.list
apt-get update
apt-get install varnish

[/code]

If compiling from source, make sure that you already have dependencies installed. Please check the official documentation for a list.

The next step is configuration. First, let’s look at the configuration file
/etc/default/varnish (Debian)

/etc/sysconfig/varnish (Red Hat)

 

This holds a lot of useful and important settings for the Varnish daemon, such as locked shared memory, default TTL, numbers of threads, etc.

Here is a simple example:

[code]

NFILES=131072
MEMLOCK=82000
NPROCS="unlimited"
RELOAD_VCL=1
VARNISH_VCL_CONF=/etc/varnish/default.vcl
VARNISH_LISTEN_PORT=6081
VARNISH_ADMIN_LISTEN_ADDRESS=127.0.0.1
VARNISH_ADMIN_LISTEN_PORT=6082
VARNISH_SECRET_FILE=/etc/varnish/secret
VARNISH_MIN_THREADS=10
VARNISH_MAX_THREADS=50
VARNISH_THREAD_TIMEOUT=120
VARNISH_STORAGE_SIZE=128M
VARNISH_STORAGE="malloc,${VARNISH_STORAGE_SIZE}"
VARNISH_TTL=120
DAEMON_OPTS="-a ${VARNISH_LISTEN_ADDRESS}:${VARNISH_LISTEN_PORT} \
             -f ${VARNISH_VCL_CONF} \
             -T ${VARNISH_ADMIN_LISTEN_ADDRESS}:${VARNISH_ADMIN_LISTEN_PORT} \
             -t ${VARNISH_TTL} \
             -p esi_syntax=0x2 \
             -p cli_buffer=16384 \
             -p http_resp_hdr_len=131072 \
             -p thread_pools=4 \
             -p thread_pool_add_delay=2 \
             -p thread_pool_min=15 \
             -p thread_pool_max=50 \
             -p thread_pool_timeout=3600 \
             -u varnish -g varnish \
             -S ${VARNISH_SECRET_FILE} \
             -s ${VARNISH_STORAGE}"

[/code]

In VARNISH_VCL_CONF we define a path to a second file, which describes request handling and the caching process in the Varnish Configuration Language (VCL). VCL is translated to C and compiled into binary code which is then executed when requests arrive. This feature makes Varnish a powerful HTTP processor not only for caching, since VCL has inherited a lot from C and it reads much like a simple C or Perl script. We will look on it closer in next chapter.

 

A look inside

Let’s look inside request processing in Varnish v.4!

To start, here’s a simplified diagram of the Varnish Finite State Machine and a brief description of each of its processes:

 

varnish processes

 

VCL_RECV

The start of all processes, called just after the request has been received and parsed. It determines the next steps for incoming requests – will they be modified, cached or not, should anything be forwarded for processing by the backend or simply return a response from cache, etc.

VCL_HASH

Called after vcl_recv to create a hash value for the request that will be used as a lookup key for the object in Varnish. Passes control to the functions, depended on vcl_recv result.

VCL_HIT

Called when a cache lookup is successful. Its result determines how to handle the rest of the request.

VCL_MISS

Called after a cache lookup if the requested document was not found in cache or if vcl_hit returned fetch.

VCL_PASS

Called upon entering pass mode. In this mode, the request is passed on to the backend, and the backend’s response is passed on to the client, but is not entered into the cache.

VCL_PIPE

Called upon entering pipe mode. In this mode, the request is passed on to the backend, and any further data from both the client and backend is passed on unaltered until either end closes the connection.

 

Backend servers

In the descriptions above, we referred to the backend several times. In Varnish terminology “backend” refers to any server(s) that provide cacheable content for Varnish, the one(s) that Varnish accelerates.

To tell Varnish where it can find a backend, define it in the VCL file like this:

[code]

backend default {
        .host = "127.0.0.1";
        .port = "8080";
}

[/code]

 

In real life we could encounter server crashes or other problems in the backend. To ensure continuity of work we will define a set of parameters called “probe” for checking the backend’s state.

[code]

backend default{
        .host = "127.0.0.1";
        .port = "8000";
        .probe = {
                .url = "/alive/";
                .timeout = 2s;
                .interval = 3s;
                .window = 10;
                .threshold = 9;
        }
}

[/code]

 

Now Varnish will send a simple GET request to “/alive/” url every 3 seconds, with a timeout limit of 2 seconds. Since we set “window” to 10 and “threshold” to 9, the backend will be considered healthy if at least 9 of the last 10 requests succeeded. Varnish will not send traffic to hosts that are marked as unhealthy.

Great, now when Varnish needs content it will know where to get it by using a connection to a default backend. But what if you have multiple backend servers or maybe you have some special policy for some requests? We could define a new backend called ‘’magic’’ and add a new rule for processing requests:

 

[code]

backend magic {
        .host = "127.0.0.1";
        .port = "8000";
}
...
sub vcl_recv {
        if (req.url ~ "^/magic/") {
                set req.backend_hint = magic;
        } else {
                set req.backend_hint = default;
        }
}

[/code]

 

You are free to define your own method of specifying a backend for a particular request, based on rules, parameters or randomly. You can also group several backend servers together into a “director”. This requires you to load a VMOD, a Varnish module, and then call certain actions in vcl_init.

 

[code]

import directors;    # load the directors
 
backend default {
        .host = "127.0.0.1";
        .port = "8080";
}
backend magic {
        .host = "127.0.0.1";
        .port = "8000";
}
 
sub vcl_init {
        new foobar = directors. round_robin();
        foobar.add_backend(default);
        foobar.add_backend(magic);
}
 
sub vcl_recv {
        set req.backend_hint = foobar.backend();
}

[/code]

This is a good way to use Varnish for load balancing. Here we are using a round-robin director, but there is also a random director which distributes requests randomly.

 

Let’s talk about Edge Side Includes (ESI)

Sometimes we need to display unique information to each user, like a user’s name, contents of their cart or a list of past orders etc. Caching such data can be a huge problem, that’s why one of the best things implemented in Varnish are ESI blocks.

Using ESI we can break our page up into smaller pieces and decide which of them should be cached or not, then assembling them into a response. These fragments can have individual cache policies, for example if you have a list of the 5 most popular products in your online store, this list can probably be cached for a time and included in other pages, while other fragments might never be cached and must always be loaded dynamically.

 

varnish esi diagram

 

With this strategy you can increase your hit rate and reduce the load on your servers.

ESI only implements a small subset of the full Varnish functionality, but it is enough. You can use three ESI statements:

  1. esi:include
  2. esi:remove
  3. <!–esi …–>

Varnish will not process ESI instructions in HTML comments.

Let’s look at some examples.

 

ESI:include

We will make a simple example. This is user.php file:

<?php
…
// Including some libraries
..
$user = $this->getUser();
echo $user->getName();
?>

 

 

Now some HTML file that has an ESI include statement. This is test.html:

<HTML>
    <BODY>
        Hello <esi:include src="user.php"/>!
    </BODY>
</HTML>

 

And the last step before ESI will work is activation ESI processing in VCL.

[code]

sub vcl_backend_response {
        if (bereq.url == "/test.html") {
               set beresp.do_esi = true;
               set beresp.ttl = 24 h;   
        } elseif (bereq.url == "/user.php") {
               set beresp.ttl = 1m;
        }
}

[/code]

 

 

ESI:remove and <!–esi … –>

 

But what should we do if ESI is not available? For that there are the <esi:remove> and <!–esi … –> constructs. You can use it to present content whether or not ESI is available, for example you can include content when ESI is available or link to it when it is not.

The ESI processors will remove the starting “<!–esi” and the ending “-→” tags when the page is processed, while still processing the contents. If the page is not processed, it will remain intact, becoming a HTML/XML comment tag. ESI processors will remove <esi:remove> tags and all content contained in them, allowing you to only render the content when the page is not being ESI-processed. For example:

<esi:remove>
  <a href="http://www.example.com/LICENSE">The license</a>
</esi:remove>
<!--esi
<p>The full text of the license:</p>
<esi:include src="http://example.com/LICENSE" />
-->

 

Who uses Varnish Cache

Based on information from Wikipedia, Varnish is used in around a tenth of the TOP 10K sites – online newspapers, social media and media content sites; here’s short a list of the most famous: Wikipedia, Facebook, Twitter, Vimeo, The New York Times, The Guardian, The Hindu, and Tumblr.

 

Use Varnish in Magento

 

Magento is the top ecommerce platform in the world. It’s known for being flexible, but not so much for being fast. If you want to use Varnish with Magento 1, you should find the ready solution for it or if you have a lot of time: Do-It-Yourself. You just need to learn how to use Magento, understand HTTP Headers and get a hold of reverse proxy caching and Varnish Cache specifically.

"There are only two hard things in Computer Science: cache invalidation and naming things."Phil Karlton

In case of Magento 2 – everything is not so bad. Magento 2 officially supports Varnish versions 3.0.5 or later or any Varnish 4.x version. Just follow the tutorial in the documentation, but we’ll summarize it for the sake of completeness:

  • Install Varnish and test it by accessing any Magento page to see if you’re getting an HTTP response header that indicates Varnish is working.
  • Install the Magento software and use the Magento Admin to create a Varnish configuration file.
  • Replace your existing Varnish configuration file with the one generated by the Admin.
  • Test everything again and if there is nothing in your /var/page_cache directory, you’ve successfully configured Varnish with Magento!

Finally, remember that your (or your customers’) ecommerce site is the last possible place on the web where you should be testing random VCL snippets and unknown PHP code.

 

 

Drupal Commerce vs. Magento: Which one suits your business?

In the following we will go through in detail the differences between the two systems: what functions they have, how can they be used, how much can they be developed and who are they ideal for?

  • The fundamental differences: content vs. ecommerce
  • Drupal Commerce and ÜberCart
    • Differences
    • Similarities
  • Magento and Drupal: the specific differences
    • Appearance, design
    • Installation
    • Administrative interface
    • Functions
    • SEO
    • Scaling
    • Developer community
  • Which one do the online stores use?
  • Summary – the final verdict

 

First of all, it is worth clarifying how much the fundamentals of Drupal and Magento are different. Drupal is intended to be a Content Management System (CMS), while Magento is an online store engine deliberately developed for carrying out ecommerce tasks. Both systems possess such basic functionalities that make them capable of performing well in their role.

 

The fundamental differences: content vs. ecommerce

 

Drupal was indeed planned primarily for content publication and management : blogs and information pages can be built on it superbly. If you want to offer different types of content on a website e.g. news, e-books, forums, social content, Drupal offers great possibilities for that. In order to be able to also give it ecommerce functionality, you have to use Drupal Commerce. However, the focus here will mainly be shifted on the sale of non-traditional products.

drupal commerce

This means that if you want to offer for example a subscription type content, or perhaps to build a system where the users have access to different pieces of content depending on the different subscription levels or packages, Drupal will completely satisfy your needs.

At the same time, Drupal Commerce is less suitable for selling more traditional types of products because there is a good chance that you will need a more complex category system and also several product groups to which lots of different attributes can be associated.

On the other hand, the Magento engine was developed specifically for the online sale of the more traditional types of products: thanks to it you will get an online store suitable to reach both retail and wholesale objectives that also supports, if needed, the commerce of digital products, too, of course.

Magento ecommerce community edition

 

Magento is at a disadvantage right where Drupal is stronger: it is not very strong in content provision, it offers few options.

If one wants to build an ecommerce site where he or she can provide content and do online commerce in one place, the most suitable system for that objective may be WordPress, as sort of a compromise solution. That is because learning to handle WordPress is quite simple, it can be considered strong in content management, it is good at search engine optimization, it can be easily integrated in social media campaigns and in addition, it can be extended with an online store module that has appropriate basic functionality thanks to WooCommerce (although this is a simpler solution than Magento would be).

 

Drupal Commerce and ÜberCart

You can choose already from two kinds of ecommerce modules made for the basics of Drupal, the older is called ÜberCart, and the name of the more recent one is Drupal Commerce.

 

ÜberCart was the very first such module that was made for Drupal 6, and it of course developed a lot during the years in functionality and operation. All in all, it became much more user friendly. Experiences show that it integrates without problems into Drupal pages, it has a built-in payment processing option and the ecommerce store can be easily installed thanks to it.

 

Differences

 

A weakness of ÜberCart is that it handles each and every product as a content node, which means that all information, all attributes are tied to the given product, which may sometimes cause errors at delivery or invoicing. For this very reason its use is simpler in case of products where there is no need to indicate variants: for example books may be like that. In case you want to upload pants for example that are sold in various sizes and colours, the different variations will go under the node “pants”, which – due to the structure of the system – can be a bit troublesome if you have a large number of products.

In case of Drupal Commerce, on the other hand, each product variant is present as a separate entity and not as a branch of the original product. In order to display the different variants in one place (for example all the colours and sizes on the page for the given pants type), you have to create a separate node (which will be the display node). This system’s basic features are more complex ÜberCart’s, but its product administration is more “traditional”.

The payment system is not part of Drupal Commerce, you have to install additional modules to be able to use these functions – at the same time payment and delivery modules can be installed optionally by default in case of ÜberCart.

 

Similarities

 

Installation of both versions is relatively simple, you will end up with a well functioning ecommerce store by using the default settings. Administration of the two systems is not very different, at least regarding the front end. Also, the online stores are completely customizable. Although learning the handling of Drupal Commerce takes more time, both ÜberCart and the more recent Drupal commercial module are relatively easy to learn.

 

Magento and Drupal: the specific differences

 

In the following, for the sake of simplicity, we will compare the capabilities of Drupal Commerce, the more recent module of Drupal that is used by more people with the capacities of Magento CE, namely the open source code, free of charge variant of Magento.

drupal commerce core features

 

Installation

 

The first more significant difference shows already at installation. Installation of Magento is an extremely simple process, it can be done in approximately ten minutes – you will come across complexity only later in case of Magento. The creation of an online store here is almost effortless.

In case of Drupal, however, users with basic knowledge may already run into problems. A separate Commerce Kickstart application is offered on the official website, which allows you to “skip a few weeks’ configuration when building your ecommerce site in the Drupal Commerce framework system”.

Kickstart finishes relatively quickly and as a result, you will end up with an ideally completely operational demo store with theme, catalogue, search features and with a back office interface, obviously without fine-tuning on the other hand – but if you are building your first store, this does not represent a significant problem.

 

Administrative interface

 

The interface of Magento is a very mature, well-tried platform that is actually easy to discover, at least when handling is considered. However, the large number of different options may be troublesome for the users who are beginners at using Magento: it is not easy to find your way around among the great deal of menus, options, possible settings; learning definitely takes time. On the other hand, several training videos, descriptions, courses are available online, which can help making the use of the basics smooth – and you can pick up later how those parts of the system work that feature more complex settings.

It also takes more time in the case of Drupal as well, to learn how exactly things work and where you can find the important stuff. At the same time, the newer versions of the content management system are much easier to handle than their predecessors were. Regarding handling it may also be an advantage that you can display the administrative module on the website as well, which means you can modify certain things more quickly this way.

 

Functions

 

Being a system originally planned to be an online store, Magento offers such additional functions that you would look for in vain in case of Drupal. We will describe some of those in detail below.

A great strength of Magento, compared to other ecommerce platforms, is that it allows narrow-down search that provides a comfortable and effective method for the users to find even among several thousands of products the one that meets most their needs.

With narrow-down search you can initiate a search based on categories, price range, brand, colour or any other variable. This is extremely important also because Magento offers unique possibilities to customize product attributes in the database. You can enter practically any kind of variable, which means that the users will be able to use the ecommerce store really comfortably only if they have the chance to search on everything. Therefore this also represents an advantage for the ecommerce stores that operate with a large number of products and also with diversified products.

Magento offers a great deal of built-in promotional options as well, which are ideal for the conversion of the visitors, for the increase of the purchase value, for the reduction of cart abandonment rate or even for the reactivation of your previous customers.

magento customer showcase

You can launch coupon sales, you can offer individual discounts on certain product groups or for given user groups (for example you can grant discounts to returning customers), you have the possibility to launch cross-sell and up-sell promotions and so on.

You can also build in a product recommending module in order to take the most out of the possibilities just mentioned. You can place CTAs that encourage the customer at a certain point of the purchasing process to order more or bigger of the given product (e.g. save money by ordering in advance the necessary quantity of a product, you can even offer discounts on that), or to add the product to the cart together with another one that other users frequently buy with it (e.g. you can offer additional storage capacity with a laptop).

The Community Edition system does not contain by default a loyalty system, the handling of loyalty points granting system, however, you can extend your ecommerce store with such functionalities as well by buying or developing such a module. You can run complex campaigns even including e-mail sending or setting up user groups. The set of tools to increase the customer lifetime value of a user to the highest possible level is at your disposal.

 

Appearance, design

 

Drupal is rich in customizing possibilities concerning its appearance and you can modify a lot of factors within the available themes as well, so you can relatively easily make your site completely unique.

drupal commerce showcase

Of course there are available designs both free of charge and commercial ones also  in case of Magento, but it goes without saying that it is giving a totally individual appearance to your online store what you will benefit the most from. In this latter case, if you have a bit bigger budget, you can create a really unique and special appearance with the assistance of the appropriate professionals. The system of Magento is quite complex and delicate though, so it is not advisable for any amateur to start experimenting with a major redesign.

 

SEO

 

Both systems are basically good in search engine optimization, but Drupal has an advantage in this field regarding that it operates as a content management system by default. Nevertheless, you do not need to give up on ranking high on the hit lists in case of Magento either, the system, as a whole, is very well optimized. On the other hand, experiences show that you may run into difficulties in case of certain issues (that is why it may be more practical and at the same time more expensive and more time-consuming to use your own developments with the help of professionals).

 

Scaling

 

The advantage is clearly at Magento in this respect. It is not by chance that the ecommerce stores selling a lot of products and lots of their variants are built rather on this system and rarely on Drupal. As we already discussed it, the strength of Drupal is content and it is especially ideal when you want to sell a low number of products with a few different attributes, like books.

Magento is capable of handling several thousands of products practically without problems. You can assign loads of different (even completely unique) attributes to these, which are also easily searchable. So if you are ambitious, wanting to sell the complete product range of a large company, or you are building such an online store where you sell a large number of products by default, the balance tips in favour of Magento.

 

Developer community

 

There is a strong and active international community behind both systems who carry out creative developments on them. The direction of these developments is fundamentally different though: while ecommerce functionality is in the focus in case of Magento, the developers dealing with Drupal are typically broadening, fine-tuning or rethinking CMS functions – therefore few of them deal with the ecommerce modules.

 

Which one do the online stores use?

 

Magento has been one of the most popular online store engines for years, according to the surveys, only the WordPress system WooCommerce can outstrip its share in ecommerce. Websites with a high number of visitors and the large companies mainly use the system of Magento.

Based on the latest data, approximately one fourth of all ecommerce stores worldwide use Magento, while the share of Drupal Commerce and ÜberCart is almost unmeasurable – they do not even figure in most of the surveys or are included in the “other” category.

 

SUMMARY

If you want to build a website where you promote a lower number of products primarily with content marketing tools, Drupal operating as a content management system may be a better solution. However, if content is secondary, so for example you only want to write a blog on the site, and in the meantime you offer a significant product range and you need more complex ecommerce store functionality, Magento is the more optimal choice.

 

drupal content management system

 

For beginners, it is also Drupal that is recommended, as it is simpler and not least cheaper to customize, to shape – although major redesign is not advisable for amateurs here either. In case of Magento though, you will definitely need the assistance of a developer.

Large companies, major commercial chains selling their products on the Internet, so the ones with greater needs, will surely find that it is worth investing in the creation of a properly optimized Magento site.

 

 

How many users can your online store handle simultaneously? Test it!

What is performance testing?

According to the ISTQB (International Software Testing and Qualification Board) performance testing is “Testing to determine the performance of a software product.

According to Wikipedia: “… a testing practice performed to determine how a system performs in terms of responsiveness and stability under a particular workload. It can also serve to investigate, measure, validate or verify other quality attributes of the system, such as scalability, reliability and resource usage.”

So, this is such a non functional testing process that deals with the performance of the application (software, i.e. a Magento online store) and can be used throughout the development process for detecting and preventing problems.

In my opinion, Wikipedia’s definition is a closer hit, however, giving a perfect definition, just like in many other cases, is impossible.

 

Why is performance testing necessary?

Today Internet penetration is globally widespread so more and more people buy products online on ecommerce stores. A lot of customers, instead of visiting “normal” shops, trying physical products and asking for information from the staff, look for products online, compare and examine them like “shopping experts” checking out various e-stores around the world to find the items that best suit their needs. As holidays and special deals (e.g. Christmas or Black Friday) are approaching, more and more customers are visiting online stores rather than their brick-and-mortar counterparts.

Your store has to stand the increased or continuously heavy traffic, therefore performance testing is crucial.

Your ecommerce store is made up of different components, it has software, hardware and network related parts, which can be broken down into sub sections, such as system framework, processor performance of the server or data quantity of the downloaded page. If you do not pay attention to all this, your e-store can slow down in case of an increased traffic period, or even can be inaccessible or, in the worst case, can crash completely.

 

Types of PET

Below, we take a look at the different kinds of relevant performance testing, highlighting the most important types of testing in case of online stores. (For further information, I recommend reading Wikipedia’s Software testing article.)

1. Load testing: This is the simplest form of performance testing, since we measure how the application behaves under normal or higher load.

a) Endurance testing: It measures system operation under continuous load for a longer period of time, so the errors that may stay hidden after a few hours’ testing, can be revealed after several days of testing.

2. Stress testing: Ideally, it is used for identifying the upper limits of capacity and the breakpoints. This type of testing helps determining the system’s robustness under extreme load and helps the administrators identify the ideal and maximum scope of operation.

a) Capacity testing: With stress testing, we measure the amount of queries/actions/users the system can handle simultaneously without faults.

3. Soak testing: Soak testing or endurance testing is simulating a normal system operation where we examine how well the system can sustain a continuous, normal load. During the test, memory utilization also has to be kept an eye on so that memory leak issues can be detected.

4. Spike testing: We give a sudden burst of load to the system and reduce the load just as fast. This quick change is shown as a spike in the load chart, hence its name.

5. Configuration testing: You may ask what this kind of testing has to do with performance testing. It is useful because it examines how configuration settings influence the whole system or parts of the system, primarily in terms of performance.

6. Isolation testing: It is not a unique type of testing to performance testing. It is a repetitive test which detects a system error. These tests can often isolate the fault domain and environment.

Performance testing types

Types of performance testing

 

Steps of testing

  1. Identifying and creating the test environment
  2. Identifying acceptance criteria
  3. Planning tests (writing testing scenarios)
  4. Configuring the test environment (data upload, setting parameters)
  5. Implementing tests
  6. Running tests
  7. Evaluating results, making reports, retesting

 

02-Tsung-performance-testing-logo

 

Tsung (IDX-Tsunami 1.6.0)

Tsung is a distributed load and stress testing tool. It is protocol dependant and can be run on servers using the following protocol communication solutions:

 

The main advantage of Tsung is that it can simulate traffic with a great number of visitors from a single computer. If we use it on a set of computers (cluster), it performs remarkably well while offering easy configuration and maintenance.

Tsung flowcart

Flowchart of Tsung’s operation

 

Characteristics

  • High performance
  • Distributed
  • Multi-protocols
  • SSL support
  • Allocation of several IP addresses on a single machine
  • Monitoring the operating system during testing
  • XML configuration system
  • Dynamic scenarios (transactions)
  • Mixed user behaviours (work processes or sessions)
  • Stochastic processes (thinktimes)

 

What is Erlang and why is it important?

Tsung is developed in Erlang, which makes Tsung very competent since it is a concurrency oriented programming language. Erlang OTP (Open Transaction Platform) serves as the basis of Tsung offering the following features:

 

  • Performance
  • Scalability
  • Fault tolerance

 

Protocols and performance

Tsung is capable of reaching high performance if provided with an appropriate background. This means the following in numbers:

 

  • Jabber/XMPP protocol:
    • 90,000 simultaneous Jabber users on a 4-node Tsung cluster
    • 10,000 simultaneous users. Tsung was running on a 3-computer cluster (800MHz CPU).

 

  • HTTP and HTTPS protocol:
    • 12,000 simultaneous users. Tsung were running on a 4-computer cluster (in 2003). 3000 requests/seconds.
    • 10,000,000 simultaneous users: Tsung with a 75-computer cluster, more than 1,000,000 queries/second.

 

How to use Tsung

First you need to install Tsung to your server for which the service of Amazon EC2 – Virtual Server Hosting is a convenient solution.

Installing Tsung

Now we take a look at how to install Tsung. VPS server configuration is the following:

 

  • CentOS 6.7 operating system
  • CPU: 8 core (Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz)
  • Memory: 3 GB
  • Storage: 10 GB HDD

 

  1. First, you need to install Erlang and Firefox since reports are generated through them.
 [root@aion-tsung ~]# yum -y install erlang perl perl-RRD-Simple.noarch perl-Log-Log4perl-RRDs.noarch gnuplot perl-Template-Toolkit firefox 

 

      1. Then download and install Tsung:

[root@aion-tsung ~]# wget http://tsung.erlang-projects.org/dist/tsung-1.6.0.tar.gz
[root@aion-tsung ~]# tar zxfv tsung-1.6.0.tar.gz
[root@aion-tsung ~]# cd tsung-1.6.0
[root@aion-tsung ~]# ./configure && make && make install

 

      1. For the Tsung report generation command create a pre-set command (alias command) for easier use (using VIM):

[root@aion-tsung ~]# vim ~/.bashrc
vim > alias treport="/usr/lib/tsung/bin/tsung_stats.pl; firefox report.html"
vim > :w
vim > :q
[root@aion-tsung ~]# source ~/.bashrc

 

      1. Prepare Tsung for the first use (optional):

[root@aion-tsung ~]# cd /root/
[root@aion-tsung ~]# mkdir .tsung
[root@aion-tsung ~]# cd ..
[root@aion-tsung ~]# cp /usr/share/doc/tsung/examples/http_simple.xml /root/.tsung/tsung.xml

 

Configuring Tsung (/root/.tsung/tsung.xml)

The first step of the configuration file is relatively fixed, but processes and monitoring can be well managed with the help of workflows and transactions.

Let’s take a look at this file as an example, examples/http_simple.xml:

<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/usr/share/tsung/tsung-1.0.dtd">
<tsung loglevel="notice" version="1.0">

    <!-- Client side setup -->
    <clients>
        <client host="localhost" use_controller_vm="true"/>
    </clients>

    <!-- Server side setup -->
    <servers>
        <server host="195.56.150.103" port="80" type="tcp"></server>
    </servers>

    <!-- to start os monitoring (cpu, network, memory) -->
    <monitoring>
        <monitor host="195.56.150.103" type="snmp"></monitor>
    </monitoring>

    <load>
        <!-- several arrival phases can be set: for each phase, you can set
        the mean inter-arrival time between new clients and the phase
        duration -->
        <arrivalphase phase="1" duration="10" unit="minute">
            <users interarrival="2" unit="second"></users>
        </arrivalphase>
    </load>

    <options>
        <option type="ts_http" name="user_agent">
            <user_agent probability="80">Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.8) Gecko/20050513 Galeon/1.3.21</user_agent>
            <user_agent probability="20">Mozilla/5.0 (Windows; U; Windows NT 5.2; fr-FR; rv:1.7.8) Gecko/20050511 Firefox/1.0.4</user_agent>
        </option>
    </options>

    <!-- start a session for a http user. the probability is the
    frequency of this type os session. The sum of all session's
    probabilities must be 100 -->

    <sessions>
        <session name="http-example" probability="100" type="ts_http">

            <!-- full url with server name, this overrides the "server" config value -->
            <request>
                <http url="/" method="GET" version="1.1"></http>
            </request>
            <request>
                <http url="/images/accueil1.gif" method="GET" version="1.1" if_modified_since="Fri, 14 Nov 2003 02:43:31 GMT"></http>
            </request>
            <request>
                <http url="/images/accueil2.gif" method="GET" version="1.1" if_modified_since="Fri, 14 Nov 2003 02:43:31 GMT"></http>
            </request>
            <request>
                <http url="/images/accueil3.gif" method="GET" version="1.1" if_modified_since="Fri, 14 Nov 2003 02:43:31 GMT"></http>
            </request>

            <thinktime value="20" random="true"></thinktime>

            <request>
                <http url="/index.en.html" method="GET" version="1.1"></http>
            </request>

        </session>
    </sessions>
</tsung>

In the above example, Tsung, installed on the server, initiates clients from localhost whose destination is port 80 of the server with the IP address 195.56.150.103. For monitoring the server, we use SNMP protocol and a Tsung client.

In the <load> section you can see that the test comprises one section which runs for 10 minutes and creates a user every 2 seconds.

The created users identify themselves as browsers in a 20-80% proportion according to the browser defined in the <options> section.

The section, describing the <sessions> work processes, defines the interaction of the users, which includes the following steps:

  1. Accessing the 56.150.103 opening page (HTTP GET)
  2. Checking on accessing the 56.150.103/images/accueil1.gif image, if it has been modified since the given time.
  3. Checking on accessing the 56.150.103/images/accueil2.gif image, if it has been modified since the given time.
  4. Checking on accessing the 56.150.103/images/accueil3.gif image, if it has been modified since the given time.
  5. Then the user is waiting for 0-20 minutes set on a random basis.
  6. Loading the 56.150.103/index.en.html page.

 

Tsung configuration options

Beyond the possibilities mentioned in the XML example, you have further options to customize your performance testing. The list below includes such options, for further details please study the configuration XML documentation of Tsung.

 

  • Upper limit of number of users (maxusers)
  • Users to be created dynamically or in a static way
  • Defining the maximum runtime of sections
  • Setting “thinking time” of users, random and hibernation settings
  • Setting time-out value for connection
  • Number of retries if connection is not re-established
  • Option for HTTP, LDAP authentication
  • Running MySQL queries
  • Changeable work process types
  • Loading and processing external files (CSV)
  • Using dynamic variables (JSONPath, Regexp, XPath, PostgreSQL)
  • Implementing iterations (for, repeat, if, foreach)

 

Parametering and running Tsung

Running Tsung is relatively simple. I definitely recommend using the Screen application so that the test runs even if VPS connection is lost.

Let’s take a look at the built-in helper, which, I believe, does not need further explanation:


[root@aion-tsung ~]# $ tsung -h
Usage: tsung <options> start|stop|debug|status
 Options:
   -f <file>     set configuration file (default is ~/.tsung/tsung.xml) (use - for standard input)
   -l <logdir>   set log directory (default is ~/.tsung/log/YYYYMMDD-HHMM/)
   -i <id>       set controller id (default is empty)
   -r <command>  set remote connector (default is ssh)
   -s                  enable erlang smp on client nodes
   -p <max>      set maximum erlang processes per vm (default is 250000)
   -m <file>     write monitoring output on this file (default is tsung.log) (use - for standard output)
   -F                  use long names (FQDN) for erlang nodes
   -w                  warmup delay (default is 10 sec)
   -v                  print version information and exit
   -6                  use IPv6 for Tsung internal communications
   -h                  display this help and exit

Running Tsung from the /root/.tsung/ directory if the configuration file, simple_website_check.xml, is the following:

 


[root@aion-tsung ~]# screen tsung -f simple_website_check.xml start

Tsung will run in a separate task after executing the command if the xml configuration file features the appropriate syntax. Tsung carries out validation before running, if it finds an error, it interrupts running and provides troubleshooting information. The log files, generated during the test, are placed in the ~/.tsung/log/YYYYMMDD-HHMM directory by default, but it can be changed by using the -1 parameter.

 

During the run phase

With version 1.6 and up, we are provided with a direct web-based monitoring option (dashboard) while running the test, which is of enormous help in terms of seeing how the testing process evolves. Thus we have the chance to interfere in time if testing starts showing extreme results or if the system is about to crash.

The dashboard can be accessed on the Tsung server via port 8091 with the following parameters:

{tsung szerver domain/ip}:8091/es/ts_web:status

Tsung Console

We are not given a lot of information about the test in the console

Tsung dashboard status

Tsung Dashboard during operation: {domain}:8091/es/ts_web:status

 

After the run phase: reporting

When our test has been run, we can find the log files (.log), the XML configuration file and the copies of attached csv files in the specified or default folder. After entering the folder and executing the treport command, which we have previously created, we can generate the HTML report containing the test results which we can view in a browser by opening the report.html file.

Before the report file is created, only the log files and the GTML page of the dashboard can be found in the folder assigned to logs:

Tsung log folder test

Content of log folder after the test run

Tsung treport run

Running treport from the console

Tsung log folder after report

Content of log folder after producing the report

 

Structure of reports

Just open the report in a browser and you will immediately see the results of the performance testing. The report is detailed enough to determine the breakpoints of the system, the inappropriate processes or their weak points. Thanks to these, we can refactor the code, scale the server or develop the application to make it stronger or faster.

Report statistics in Tsung

Statistics of the report

 

The menu on the right can be divided into two major sections. The upper contains statistical figures:

 

Main statistics

  • Transactions: summary of transactions
  • Network Throughput: throughput capacity of the network (speed / amount of data)
  • Counters: users, successful connection, phases run etc.
  • Server monitoring: result of monitoring
  • HTTP status: HTTP status codes
  • Errors: errors found

 

In the second part, we can see the graphs, however, I regard this feature as rather basic.

 

Graphs

  • Response times: Change in response times during testing.
  • Throughput graphs: Change in network load during testing.
  • Simultaneous Users: Behaviour of users simulated throughout the test.
  • Server monitoring: Usage of operating system CPU and memory.
  • HTTP status: Status of HTTP response codes during testing.
  • Errors: Errors detected during testing.

 

Tsung report graphs performance testing

Graphs of the report

 

SUMMARY

Performance testing is of high priority with ecommerce solutions, especially with Magento stores, because customers want to finish with their shopping and get their products fast, without any faults or difficulties. You have to be prepared to experience heavy traffic in holiday periods and while running a great marketing campaign. Therefore for those who would like to retain their customers and avoid having an inaccessible online store, it is deeply recommended to apply performance testing.

Tsung is a very suitable tool to carry out performance testing, either for the whole store or only for certain processes (e.g. database load, payment etc.) – while having the support of the best-known and most widespread protocols.

Thanks to its simple configuration, it is an ideal solution for performing professional testing after only a short learning effort. Using automatically generated reports, you can show the results in a simple and easy-to-interpret format to people with less technical or IT experience.

 

 

 

Magento 2 module development – A comprehensive guide – Part 2

Then we will see how to construct UI Components, introduced in Magento 2.0, and how to create definition xml, controllers, layouts and a new menu item. Finally we will describe how to create, edit, save and delete data belonging to the module.

This article will discuss the following topics:

 

1) Creating admin menu and grid

In the first step, we create the menu item in the admin area belonging to our module. We can have it in a new main menu, but it may be better to place it under a relevant main menu item. Here we place it under the Content main menu. For this, we need a new file. We create the menu in the app/code/Aion/Test/etc/adminhtml/ directory, in the menu.xml. The file contains the following:

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="Aion_Test::content_elements" title="Test Extension" module="Magento_Backend" sortOrder="10" parent="Magento_Backend::content"
             resource="Magento_Backend::content_elements"/>
        <add id="Aion_Test::aion_test" title="Manage Items" module="Aion_Test" sortOrder="10" parent="Aion_Test::content_elements" action="test/test"
             resource="Aion_Test::test"/>
    </menu>
</config>

First we define a main element in the file, namely the id=Aion_Test::content_elements and then place the menu item after this element in such a way that we define the parent of this element (id=Aion_Test::aion_test) as the main element. If everything has been executed properly, we can see our sub-menu item under the main menu Content in the admin area. Here it is important to mention the Action=”test/test” parameter, which will define the path of the adminhtml controller to be created later. Next we need to create the adminhtml controller and the layout file that will be responsible for displaying the grid. But, before that, we create an abstract controller class in order to be able to manage backend user roles in one place. We create the abstract controller class in the Test.php located in the app/code/Aion/Test/Controller/Adminhtml/ directory. The file contains the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml;

/**
 * Aion manage items controller
 */
abstract class Test extends \Magento\Backend\App\Action
{
    /**
     * Core registry
     *
     * @var \Magento\Framework\Registry
     */
    protected $_coreRegistry = null;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     */
    public function __construct(\Magento\Backend\App\Action\Context $context, \Magento\Framework\Registry $coreRegistry)
    {
        $this->_coreRegistry = $coreRegistry;
        parent::__construct($context);
    }

    /**
     * Init page
     *
     * @param \Magento\Backend\Model\View\Result\Page $resultPage
     * @return \Magento\Backend\Model\View\Result\Page
     */
    protected function initPage($resultPage)
    {
        $resultPage->setActiveMenu('Aion_Test::aion_test')
            ->addBreadcrumb(__('Test'), __('Test'))
            ->addBreadcrumb(__('Items'), __(''));
        return $resultPage;
    }

    /**
     * Check the permission to run it
     *
     * @return boolean
     */
    protected function _isAllowed()
    {
        return $this->_authorization->isAllowed('Aion_Test::test_menu');
    }
}

The initPage() function of the abstract controller is responsible for the setting of the active menu item as well as defining the breadcrumb path. The other significant function is _isAllowed(), which checks and controls admin roles. Next we create the controller needed for managing the admin grid, which we will extend from the previously mentioned abstract controller. We create the controller class in the Index.php located in the app/code/Aion/Test/Controller/Adminhtml/Test directory. The file includes the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

class Index extends \Aion\Test\Controller\Adminhtml\Test
{
    /**
     * @var \Magento\Framework\View\Result\PageFactory
     */
    protected $resultPageFactory;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory
    ) {
        $this->resultPageFactory = $resultPageFactory;
        parent::__construct($context, $coreRegistry);
    }

    /**
     * Index action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
        $resultPage = $this->resultPageFactory->create();
        $this->initPage($resultPage)->getConfig()->getTitle()->prepend(__('Items'));
        return $resultPage;
    }
}

The Index controller is responsible for displaying the admin grid. In Magento 2.0 every controller class (file) is an action indeed. This means that in our case, the IndexAction() function, known from Magento 1.x, is replaced by the execute() function. Consequently, for every controller action there is a separate controller file and one execute() function. This may immediately raise the question: what is it good for? Basically, thanks to this, the code of the whole module is much clearer than in the case of controllers in the Magento 1.x system, which often resulted in a lengthy script at the end of the development process. The $resultPage and $this->resultPageFactory replace the $this->loadLayout() and $this->renderLayout() calls, known from Magento 1.x. We need to define the path (or route) of the created adminhtml controllers in a separate file so that Magento 2.0 can “recognize” it. We define the path in the routes.xml in the app/code/Aion/Test/etc/adminhtml/ directory. The file contains the following:

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="test" frontName="test">
            <module name="Aion_Test" before="Magento_Backend" />
        </route>
    </router>
</config>

There are two important tags and parameters belonging to them in the file. The first one is <router id=”admin”>, which indicates that it is a backend path. The second one is <route id=”test” frontName=”test”>, where the frontName defines the main path of the created adminhtml controllers. Next, we need to create the collection, which will feed data to the admin grid mentioned previously. We create the collection in the Collection.php located in the app/code/Aion/Model/ResourceModel/Test/Grid/ directory. The file includes the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Model\ResourceModel\Test\Grid;

use Magento\Framework\Api\Search\SearchResultInterface;
use Magento\Framework\Search\AggregationInterface;
use Aion\Test\Model\ResourceModel\Test\Collection as TestCollection;

/**
 * Collection for displaying grid of Aion Items
 */
class Collection extends TestCollection implements SearchResultInterface
{
    /**
     * @var AggregationInterface
     */
    protected $aggregations;

    /**
     * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
     * @param \Magento\Framework\Event\ManagerInterface $eventManager
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param string $mainTable
     * @param string $eventPrefix
     * @param string $eventObject
     * @param string $resourceModel
     * @param string $model
     * @param string|null $connection
     * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource
     *
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct(
        \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        $mainTable,
        $eventPrefix,
        $eventObject,
        $resourceModel,
        $model = 'Magento\Framework\View\Element\UiComponent\DataProvider\Document',
        $connection = null,
        \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
    ) {
        parent::__construct(
            $entityFactory,
            $logger,
            $fetchStrategy,
            $eventManager,
            $storeManager,
            $connection,
            $resource
        );
        $this->_eventPrefix = $eventPrefix;
        $this->_eventObject = $eventObject;
        $this->_init($model, $resourceModel);
        $this->setMainTable($mainTable);
    }

    /**
     * @return AggregationInterface
     */
    public function getAggregations()
    {
        return $this->aggregations;
    }

    /**
     * @param AggregationInterface $aggregations
     * @return $this
     */
    public function setAggregations($aggregations)
    {
        $this->aggregations = $aggregations;
    }


    /**
     * Retrieve all ids for collection
     * Backward compatibility with EAV collection
     *
     * @param int $limit
     * @param int $offset
     * @return array
     */
    public function getAllIds($limit = null, $offset = null)
    {
        return $this->getConnection()->fetchCol($this->_getAllIdsSelect($limit, $offset), $this->_bindParams);
    }

    /**
     * Get search criteria.
     *
     * @return \Magento\Framework\Api\SearchCriteriaInterface|null
     */
    public function getSearchCriteria()
    {
        return null;
    }

    /**
     * Set search criteria.
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return $this
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function setSearchCriteria(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria = null)
    {
        return $this;
    }

    /**
     * Get total count.
     *
     * @return int
     */
    public function getTotalCount()
    {
        return $this->getSize();
    }

    /**
     * Set total count.
     *
     * @param int $totalCount
     * @return $this
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function setTotalCount($totalCount)
    {
        return $this;
    }

    /**
     * Set items list.
     *
     * @param \Magento\Framework\Api\ExtensibleDataInterface[] $items
     * @return $this
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
     */
    public function setItems(array $items = null)
    {
        return $this;
    }
}

The class defined in the file is responsible for adding the data in the table to be created, as well as implementing search and paging functions. Implementing the functions, mentioned above, is needed for the proper functioning of the UI Components to be described here. The other advantage of this is that the class we define here can be used in other locations easily, if we want to display the module’s data in an admin grid somewhere else, e.g. on a product or customer ajax tab in the admin panel. There is only one thing left, which is to create the layout file belonging to the Index controller. We create the layout file in the test_test_index.xml located in the app/code/Aion/Test/view/adminhtml/layout/ directory. The previously defined route is clearly visible in the file’s name: basic route route -> directory -> controller action. The file includes the following:

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <uiComponent name="test_test_listing"/>
        </referenceContainer>
    </body>
</page>

The name of the previously mentioned UI component file is defined in the “content” reference container located in the layout file. This will be detailed in the next section.

2) New design of UI Components or admin grid

We can create admin grids much more easily by using UI components introduced in Magento 2.0. Furthermore, this opens up more possibilities for the administrator in terms of making searches, filtering and displaying columns in a custom manner in the tables. Additionally, we can save different designs or views. In order to make the UI components functional, we need to create several files and implement them properly. The most important file, which defines the operation and design of the admin grid, is an xml file. In our module, this is named as test_test_listing.xml (see previous section) and is located in the app/code/Aion/Test/view/adminhtml/ui_component directory. This file is quite lengthy so we show it in separate sections.

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">test_test_listing.test_test_listing_data_source</item>
            <item name="deps" xsi:type="string">test_test_listing.test_test_listing_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">test_test_columns</item>
        <item name="buttons" xsi:type="array">
            <item name="add" xsi:type="array">
                <item name="name" xsi:type="string">add</item>
                <item name="label" xsi:type="string" translate="true">Add New Item</item>
                <item name="class" xsi:type="string">primary</item>
                <item name="url" xsi:type="string">*/*/new</item>
            </item>
        </item>
    </argument>
    <dataSource name="test_test_listing_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">TestGridDataProvider</argument>
            <argument name="name" xsi:type="string">test_test_listing_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">test_id</argument>
            <argument name="requestFieldName" xsi:type="string">id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="update_url" xsi:type="url" path="mui/index/render"/>
                </item>
            </argument>
        </argument>
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
            </item>
        </argument>
    </dataSource>
…

The following are defined in the first argument tag of the file:

  • data source name (test_test_listing_data_source), see: second dataSource tag: <dataSource name=”test_test_listing_data_source”>
  • colums tag name: test_test_columns, this will be needed later on
  • adding new element and defining other buttons, see: <item name=”buttons” xsi:type=”array”> tag
…
<container name="listing_top">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="template" xsi:type="string">ui/grid/toolbar</item>
        </item>
    </argument>
    <bookmark name="bookmarks">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="storageConfig" xsi:type="array">
                    <item name="namespace" xsi:type="string">test_test_listing</item>
                </item>
            </item>
        </argument>
    </bookmark>
    <component name="columns_controls">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="columnsData" xsi:type="array">
                    <item name="provider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns</item>
                </item>
                <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item>
                <item name="displayArea" xsi:type="string">dataGridActions</item>
            </item>
        </argument>
    </component>
…

Working further on in the xml file, we define the functions placed above the table. These are the following:

  • we can save the “look” of the present table in different views, see: <bookmark name=”bookmarks”> tag
  • if there are too many columns, we can define which ones should be shown and we can save this at bookmarks mentioned before, see: <component name=”columns_controls”> tag
…
<filterSearch name="fulltext">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="provider" xsi:type="string">test_test_listing.test_test_listing_data_source</item>
            <item name="chipsProvider" xsi:type="string">test_test_listing.test_test_listing.listing_top.listing_filters_chips</item>
            <item name="storageConfig" xsi:type="array">
                <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.bookmarks</item>
                <item name="namespace" xsi:type="string">current.search</item>
            </item>
        </item>
    </argument>
</filterSearch>
<filters name="listing_filters">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="columnsProvider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns</item>
            <item name="storageConfig" xsi:type="array">
                <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.bookmarks</item>
                <item name="namespace" xsi:type="string">current.filters</item>
            </item>
            <item name="templates" xsi:type="array">
                <item name="filters" xsi:type="array">
                    <item name="select" xsi:type="array">
                        <item name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</item>
                        <item name="template" xsi:type="string">ui/grid/filters/elements/ui-select</item>
                    </item>
                </item>
            </item>
            <item name="childDefaults" xsi:type="array">
                <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.listing_filters</item>
                <item name="imports" xsi:type="array">
                    <item name="visible" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.${ $.index }:visible</item>
                </item>
            </item>
        </item>
    </argument>
</filters>
…

We add the text based search function and table filters. These are the following:

  • in varchar, text type columns, we can search within an input field, see: <filterSearch name=”fulltext”> tag
  • we can filter every single column according to different parameters view(Aion\Test\Ui\Component\Listing\Column\Test\Options), select, date, ID(range), text type filters, see: <filters name=”listing_filters”> tag
…
<massaction name="listing_massaction">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="selectProvider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.ids</item>
            <item name="indexField" xsi:type="string">test_id</item>
        </item>
    </argument>
    <action name="delete">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="type" xsi:type="string">delete</item>
                <item name="label" xsi:type="string" translate="true">Delete</item>
                <item name="url" xsi:type="url" path="test/test/massDelete"/>
                <item name="confirm" xsi:type="array">
                    <item name="title" xsi:type="string" translate="true">Delete items</item>
                    <item name="message" xsi:type="string" translate="true">Are you sure you wan't to delete selected items?</item>
                </item>
            </item>
        </argument>
    </action>
    <action name="disable">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="type" xsi:type="string">disable</item>
                <item name="label" xsi:type="string" translate="true">Disable</item>
                <item name="url" xsi:type="url" path="test/test/massDisable"/>
            </item>
        </argument>
    </action>
    <action name="enable">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="type" xsi:type="string">enable</item>
                <item name="label" xsi:type="string" translate="true">Enable</item>
                <item name="url" xsi:type="url" path="test/test/massEnable"/>
            </item>
        </argument>
    </action>
</massaction>
…

The functions that we have created, needed to change mass data, are added within the massaction tag. These are the following in our module:

  • mass delete, see: <action name=”delete”> tag
  • mass enable and disable, see: <action name=”disable”> and <action name=”enable”> tags. These modify the is_active data, which was created earlier in the database table of our module.
…    
    <paging name="listing_paging">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="storageConfig" xsi:type="array">
                    <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.bookmarks</item>
                    <item name="namespace" xsi:type="string">current.paging</item>
                </item>
                <item name="selectProvider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns.ids</item>
            </item>
        </argument>
    </paging>
</container>
…

The <paging name=”listing_paging”> tag implements paging and the selectablitiy of the number of the listed elements (select) in our table.

…
<columns name="test_test_columns">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="storageConfig" xsi:type="array">
                <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.bookmarks</item>
                <item name="namespace" xsi:type="string">current</item>
            </item>
        </item>
            <item name="childDefaults" xsi:type="array">
                <item name="fieldAction" xsi:type="array">
                    <item name="provider" xsi:type="string">test_test_listing.test_test_listing.test_test_columns_editor</item>
                    <item name="target" xsi:type="string">startEdit</item>
                    <item name="params" xsi:type="array">
                        <item name="0" xsi:type="string">${ $.$data.rowIndex }</item>
                        <item name="1" xsi:type="boolean">true</item>
                    </item>
                </item>
                <item name="storageConfig" xsi:type="array">
                    <item name="provider" xsi:type="string">test_test_listing.test_test_listing.listing_top.bookmarks</item>
                    <item name="root" xsi:type="string">columns.${ $.index }</item>
                    <item name="namespace" xsi:type="string">current.${ $.storageConfig.root }</item>
                </item>
            </item>
        </item>
    </argument>
    <selectionsColumn name="ids">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="indexField" xsi:type="string">test_id</item>
            </item>
        </argument>
    </selectionsColumn>
…

Now we define the columns of the table, see: <columns name=”test_test_columns”> tag. Its name was defined at the beginning of the file. The ID field, set with the mass actions, mentioned earlier, see: <selectionsColumn name=”ids”> tag.

…   
        <column name="test_id">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">textRange</item>
                    <item name="sorting" xsi:type="string">asc</item>
                    <item name="label" xsi:type="string" translate="true">ID</item>
                </item>
            </argument>
        </column>
        <column name="name">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="editor" xsi:type="array">
                        <item name="editorType" xsi:type="string">text</item>
                        <item name="validation" xsi:type="array">
                            <item name="required-entry" xsi:type="boolean">true</item>
                        </item>
                    </item>
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Name</item>
                </item>
            </argument>
        </column>
        <column name="email">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="editor" xsi:type="array">
                        <item name="editorType" xsi:type="string">text</item>
                        <item name="validation" xsi:type="array">
                            <item name="required-entry" xsi:type="boolean">true</item>
                        </item>
                    </item>
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Email</item>
                </item>
            </argument>
        </column>
        <column name="creation_time" class="Magento\Ui\Component\Listing\Columns\Date">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Created</item>
                </item>
            </argument>
        </column>
        <column name="update_time" class="Magento\Ui\Component\Listing\Columns\Date">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Modified</item>
                </item>
            </argument>
        </column>
        <column name="sort_order">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="editor" xsi:type="array">
                        <item name="editorType" xsi:type="string">text</item>
                        <item name="validation" xsi:type="array">
                            <item name="required-entry" xsi:type="boolean">true</item>
                        </item>
                    </item>
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Sort Order</item>
                </item>
            </argument>
        </column>
        <column name="is_active">
            <argument name="data" xsi:type="array">
                <item name="options" xsi:type="array">
                    <item name="disable" xsi:type="array">
                        <item name="value" xsi:type="string">0</item>
                        <item name="label" xsi:type="string" translate="true">Disabled</item>
                    </item>
                    <item name="enable" xsi:type="array">
                        <item name="value" xsi:type="string">1</item>
                        <item name="label" xsi:type="string" translate="true">Enabled</item>
                    </item>
                </item>
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">select</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/select</item>
                    <item name="editor" xsi:type="string">select</item>
                    <item name="dataType" xsi:type="string">select</item>
                    <item name="label" xsi:type="string" translate="true">Status</item>
                </item>
            </argument>
        </column>
        <actionsColumn name="actions" class="Aion\Test\Ui\Component\Listing\Column\TestActions">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">test_id</item>
                </item>
            </argument>
        </actionsColumn>
    </columns>
</listing>

Next we need to define the columns in the table. With each column we can set the type, e.g. text, select, textRange etc. The last column includes the basic actions, see: <actionsColumn name=”actions” class=”Aion\Test\Ui\Component\Listing\Column\TestActions”> tag Now we have finished with the xml defining the grid (test_test_listing.xml). Now we will take a look at some classes which are responsible for the actions located in the last column.

3) UI component classes

For the functioning of the action column located in the grid defining xml, created in the previous section, we need a class which assists in displaying and functioning. The first one is the TestActions class seen in the previous section tag, <actionsColumn name=”actions” class=”Aion\Test\Ui\Component\Listing\Column\TestActions”>. The file is named as TestActions.php located in the app/code/Aion/Test/Ui/Component/Listing/Column directory. The file contains the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Ui\Component\Listing\Column;

use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;

/**
 * Class TestActions
 */
class TestActions extends Column
{
    /**
     * Url path
     */
    const URL_PATH_EDIT = 'test/test/edit';
    const URL_PATH_DELETE = 'test/test/delete';

    /**
     * @var UrlInterface
     */
    protected $urlBuilder;

    /**
     * Constructor
     *
     * @param ContextInterface $context
     * @param UiComponentFactory $uiComponentFactory
     * @param UrlInterface $urlBuilder
     * @param array $components
     * @param array $data
     */
    public function __construct(
        ContextInterface $context,
        UiComponentFactory $uiComponentFactory,
        UrlInterface $urlBuilder,
        array $components = [],
        array $data = []
    ) {
        $this->urlBuilder = $urlBuilder;
        parent::__construct($context, $uiComponentFactory, $components, $data);
    }

    /**
     * Prepare Data Source
     *
     * @param array $dataSource
     * @return array
     */
    public function prepareDataSource(array $dataSource)
    {
        if (isset($dataSource['data']['items'])) {
            foreach ($dataSource['data']['items'] as & $item) {
                if (isset($item['test_id'])) {
                    $item[$this->getData('name')] = [
                        'edit' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_EDIT,
                                [
                                    'test_id' => $item['test_id']
                                ]
                            ),
                            'label' => __('Edit')
                        ],
                        'delete' => [
                            'href' => $this->urlBuilder->getUrl(
                                static::URL_PATH_DELETE,
                                [
                                    'test_id' => $item['test_id']
                                ]
                            ),
                            'label' => __('Delete'),
                            'confirm' => [
                                'title' => __('Delete "${ $.$data.name }"'),
                                'message' => __('Are you sure you wan\'t to delete a "${ $.$data.name }" record?')
                            ]
                        ]
                    ];
                }
            }
        }

        return $dataSource;
    }
}

The class creates the array in the appropriate format, necessary for displaying the mass action. It is important to define the precise path with the constant values at the beginning of the file in order to direct to the proper adminhtml controllers.

4) Adminhtml controllers

For the complete functioning of the grid, a few controllers need to be created. Let’s see them one by one. For mass deletion, we use the massDelete controller. The file is named as MassDelete.php located in the app/code/Aion/Test/Controller/Adminhtml/Test/ directory. The file includes the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

use Magento\Framework\Controller\ResultFactory;
use Magento\Backend\App\Action\Context;
use Magento\Ui\Component\MassAction\Filter;
use Aion\Test\Model\ResourceModel\Test\CollectionFactory;

/**
 * Class MassDelete
 */
class MassDelete extends \Magento\Backend\App\Action
{
    /**
     * @var Filter
     */
    protected $filter;

    /**
     * @var CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @param Context $context
     * @param Filter $filter
     * @param CollectionFactory $collectionFactory
     */
    public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory)
    {
        $this->filter = $filter;
        $this->collectionFactory = $collectionFactory;
        parent::__construct($context);
    }

    /**
     * Execute action
     *
     * @return \Magento\Backend\Model\View\Result\Redirect
     * @throws \Magento\Framework\Exception\LocalizedException|\Exception
     */
    public function execute()
    {
        $collection = $this->filter->getCollection($this->collectionFactory->create());
        $collectionSize = $collection->getSize();

        foreach ($collection as $item) {
            $item->delete();
        }

        $this->messageManager->addSuccess(__('A total of %1 record(s) have been deleted.', $collectionSize));

        /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
        $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
        return $resultRedirect->setPath('*/*/');
    }
}

The execute() function of the controller class (namely the action) is given a controller (from the \Magento\Ui\Component\MassAction\Filter class), deleting the items iterating through it. For modifying the mass status, we use the massEnable and massDisable controllers. The files are named as MassEnable.php and MassDisable.php located in the app/code/Aion/Test/Controller/Adminhtml/Test/ directory. The files contain the following:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

use Magento\Framework\Controller\ResultFactory;
use Magento\Backend\App\Action\Context;
use Magento\Ui\Component\MassAction\Filter;
use Aion\Test\Model\ResourceModel\Test\CollectionFactory;

/**
 * Class MassEnable
 */
class MassEnable extends \Magento\Backend\App\Action
{
    /**
     * @var Filter
     */
    protected $filter;

    /**
     * @var CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @param Context $context
     * @param Filter $filter
     * @param CollectionFactory $collectionFactory
     */
    public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory)
    {
        $this->filter = $filter;
        $this->collectionFactory = $collectionFactory;
        parent::__construct($context);
    }

    /**
     * Execute action
     *
     * @return \Magento\Backend\Model\View\Result\Redirect
     * @throws \Magento\Framework\Exception\LocalizedException|\Exception
     */
    public function execute()
    {
        $collection = $this->filter->getCollection($this->collectionFactory->create());

        foreach ($collection as $item) {
            $item->setIsActive(true);
            $item->save();
        }

        $this->messageManager->addSuccess(__('A total of %1 record(s) have been enabled.', $collection->getSize()));

        /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
        $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
        return $resultRedirect->setPath('*/*/');
    }
}
<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

use Magento\Framework\Controller\ResultFactory;
use Magento\Backend\App\Action\Context;
use Magento\Ui\Component\MassAction\Filter;
use Aion\Test\Model\ResourceModel\Test\CollectionFactory;

/**
 * Class MassDisable
 */
class MassDisable extends \Magento\Backend\App\Action
{
    /**
     * @var Filter
     */
    protected $filter;

    /**
     * @var CollectionFactory
     */
    protected $collectionFactory;

    /**
     * @param Context $context
     * @param Filter $filter
     * @param CollectionFactory $collectionFactory
     */
    public function __construct(Context $context, Filter $filter, CollectionFactory $collectionFactory)
    {
        $this->filter = $filter;
        $this->collectionFactory = $collectionFactory;
        parent::__construct($context);
    }

    /**
     * Execute action
     *
     * @return \Magento\Backend\Model\View\Result\Redirect
     * @throws \Magento\Framework\Exception\LocalizedException|\Exception
     */
    public function execute()
    {
        $collection = $this->filter->getCollection($this->collectionFactory->create());

        foreach ($collection as $item) {
            $item->setIsActive(false);
            $item->save();
        }

        $this->messageManager->addSuccess(__('A total of %1 record(s) have been disabled.', $collection->getSize()));

        /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
        $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
        return $resultRedirect->setPath('*/*/');
    }
}

The two types of functioning of the two controllers are very similar. Both iterate through the collection provided by the Filter class and set the is_active data key to TRUE in case of massEnbale, and to FALSE in case of massDisable, and then save the elements of the collection.

5) Object manager configuration

For the proper functioning of the admin grid, we need to define the source data objects and filters. For this, we need a defining xml. The file is located in the app/code/Aion/Test/etc/ directory, named as di.xml. The file includes the following:

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="test_test_listing_data_source" xsi:type="string">Aion\Test\Model\ResourceModel\Test\Grid\Collection</item>
            </argument>
        </arguments>
    </type>
    <type name="Aion\Test\Model\ResourceModel\Test\Grid\Collection">
        <arguments>
            <argument name="mainTable" xsi:type="string">aion_test</argument>
            <argument name="eventPrefix" xsi:type="string">aion_test_grid_collection</argument>
            <argument name="eventObject" xsi:type="string">test_grid_collection</argument>
            <argument name="resourceModel" xsi:type="string">Aion\Test\Model\ResourceModel\Test</argument>
        </arguments>
    </type>
    <virtualType name="TestGirdFilterPool" type="Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool">
        <arguments>
            <argument name="appliers" xsi:type="array">
                <item name="regular" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\RegularFilter</item>
                <item name="fulltext" xsi:type="object">Magento\Framework\View\Element\UiComponent\DataProvider\FulltextFilter</item>
            </argument>
        </arguments>
    </virtualType>
    <virtualType name="TestGridDataProvider" type="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider">
        <arguments>
            <argument name="collection" xsi:type="object" shared="false">Aion\Test\Model\ResourceModel\Test\Collection</argument>
            <argument name="filterPool" xsi:type="object" shared="false">TestGirdFilterPool</argument>
        </arguments>
    </virtualType>
</config>

Here we define the collection needed for the grid (see: <item name=”test_test_listing_data_source” xsi:type=”string”>Aion\Test\Model\ResourceModel\Test\Grid\Collection</item>), the filter and data provider that are necessary for the proper functioning of the UI component.   We describe editing, saving and deleting of the different elements in the following sections.

6) Creating admin blocks necessary for editing

In order to be able to create the data, in the admin panel, belonging to the module and also be able to edit them, we need the appropriate classes. First, we need to create the container class, which will contain the form later on. We create the class in the Test.php file located in the Aion/Test/Block/Adminhtml/ directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Block\Adminhtml;

/**
 * Adminhtml Aion items content block
 */
class Test extends \Magento\Backend\Block\Widget\Grid\Container
{
    /**
     * @return void
     */
    protected function _construct()
    {
        $this->_blockGroup = 'Aion_Test';
        $this->_controller = 'adminhtml_test';
        $this->_headerText = __('Items');
        $this->_addButtonLabel = __('Add New Item');
        parent::_construct();
    }
}

It is important to define the proper blockGroup and controller. We now need the form container class. Here we define the title of the admin page of the edited object and can add or remove custom buttons to it besides the “basic” buttons. We create the class in the Edit.php file located in the Aion/Test/Block/Adminhtml/Test directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Block\Adminhtml\Test;

/**
 * Aion item edit form container
 */
class Edit extends \Magento\Backend\Block\Widget\Form\Container
{
    /**
     * Core registry
     *
     * @var \Magento\Framework\Registry
     */
    protected $_coreRegistry = null;

    /**
     * @param \Magento\Backend\Block\Widget\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param array $data
     */
    public function __construct(
        \Magento\Backend\Block\Widget\Context $context,
        \Magento\Framework\Registry $registry,
        array $data = []
    ) {
        $this->_coreRegistry = $registry;
        parent::__construct($context, $data);
    }

    /**
     * @return void
     */
    protected function _construct()
    {
        $this->_objectId = 'test_id';
        $this->_blockGroup = 'Aion_Test';
        $this->_controller = 'adminhtml_test';

        parent::_construct();

        $this->buttonList->update('save', 'label', __('Save Item'));
        $this->buttonList->update('delete', 'label', __('Delete Item'));

        $this->buttonList->add(
            'saveandcontinue',
            [
                'label' => __('Save and Continue Edit'),
                'class' => 'save',
                'data_attribute' => [
                    'mage-init' => ['button' => ['event' => 'saveAndContinueEdit', 'target' => '#edit_form']],
                ]
            ],
            -100
        );

    }

    /**
     * Get edit form container header text
     *
     * @return \Magento\Framework\Phrase
     */
    public function getHeaderText()
    {
        if ($this->_coreRegistry->registry('test_item')->getId()) {
            return __("Edit Block '%1'", $this->escapeHtml($this->_coreRegistry->registry('test_item')->getName()));
        } else {
            return __('New Item');
        }
    }
}

If we want to use WYSWYG editor with textarea type fields for example, then it needs to be placed in the _construct() function or in the prepareLayout() function. The title value of the admin page is defined by the getHeaderText() function in the class. The last block that needs to be created displays and manages the form. We create the class in the Form.php file located in the Aion/Test/Block/Adminhtml/Test/Edit directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Block\Adminhtml\Test\Edit;

/**
 * Adminhtml Aion item edit form
 */
class Form extends \Magento\Backend\Block\Widget\Form\Generic
{
    /**
     * @var \Magento\Cms\Model\Wysiwyg\Config
     */
    protected $_wysiwygConfig;

    /**
     * @var \Magento\Store\Model\System\Store
     */
    protected $_systemStore;

    /**
     * @param \Magento\Backend\Block\Template\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param \Magento\Framework\Data\FormFactory $formFactory
     * @param \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig
     * @param \Magento\Store\Model\System\Store $systemStore
     * @param array $data
     */
    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Framework\Registry $registry,
        \Magento\Framework\Data\FormFactory $formFactory,
        \Magento\Cms\Model\Wysiwyg\Config $wysiwygConfig,
        \Magento\Store\Model\System\Store $systemStore,
        array $data = []
    ) {
        $this->_wysiwygConfig = $wysiwygConfig;
        $this->_systemStore = $systemStore;
        parent::__construct($context, $registry, $formFactory, $data);
    }

    /**
     * Init form
     *
     * @return void
     */
    protected function _construct()
    {
        parent::_construct();
        $this->setId('test_form');
        $this->setTitle(__('Item Information'));
    }

    /**
     * Prepare form
     *
     * @return $this
     */
    protected function _prepareForm()
    {
        $model = $this->_coreRegistry->registry('test_item');

        /** @var \Magento\Framework\Data\Form $form */
        $form = $this->_formFactory->create(
            ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
        );

        $form->setHtmlIdPrefix('item_');

        $fieldset = $form->addFieldset(
            'base_fieldset',
            ['legend' => __('General Information'), 'class' => 'fieldset-wide']
        );

        if ($model->getId()) {
            $fieldset->addField('test_id', 'hidden', ['name' => 'test_id']);
        }

        $fieldset->addField(
            'name',
            'text',
            [
                'name' => 'name',
                'label' => __('Name'),
                'title' => __('Name'),
                'required' => true
            ]
        );

        $fieldset->addField(
            'email',
            'text',
            [
                'name' => 'email',
                'label' => __('Email'),
                'title' => __('Email'),
                'required' => true,
                'class' => 'validate-email'
            ]
        );

        $fieldset->addField(
            'is_active',
            'select',
            [
                'label' => __('Status'),
                'title' => __('Status'),
                'name' => 'is_active',
                'required' => true,
                'options' => ['1' => __('Enabled'), '0' => __('Disabled')]
            ]
        );
        if (!$model->getId()) {
            $model->setData('is_active', '1');
        }

        $fieldset->addField(
            'sort_order',
            'text',
            [
                'name' => 'sort_order',
                'label' => __('Sort Order'),
                'title' => __('Sort Order'),
                'required' => false
            ]
        );

        $form->setValues($model->getData());
        $form->setUseContainer(true);
        $this->setForm($form);

        return parent::_prepareForm();
    }
}

We add the fields we want to edit in the _prepareForm() function of the class. These, in our case, are the name, email and sort_order fields. Additionally, there is the store_id field (important for multistore management) and the is_active field, which is of select type at present and is used for setting the status of the element which is being edited. Having finished with the above three classes, we have created the files necessary for the editing functions in the admin panel.

7) Creating controllers and layout

Apart from the classes mentioned previously, we still need the appropriate controller classes and layout files for the editing process. We create the first class in the NewAction.php file in the Aion/Test/Controller/Adminhtml/Test/ directory.

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

class NewAction extends \Aion\Test\Controller\Adminhtml\Test
{
    /**
     * @var \Magento\Backend\Model\View\Result\ForwardFactory
     */
    protected $resultForwardFactory;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Backend\Model\View\Result\ForwardFactory $resultForwardFactory
    ) {
        $this->resultForwardFactory = $resultForwardFactory;
        parent::__construct($context, $coreRegistry);
    }

    /**
     * Create new item
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        /** @var \Magento\Framework\Controller\Result\Forward $resultForward */
        $resultForward = $this->resultForwardFactory->create();
        return $resultForward->forward('edit');
    }
}

The class serves the creation of new elements and basically the function of the action (execute()) redirects to the Edit controller class. Next we create the controller needed for editing. We create the class in the Edit.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

class Edit extends \Aion\Test\Controller\Adminhtml\Test
{
    /**
     * @var \Magento\Framework\View\Result\PageFactory
     */
    protected $resultPageFactory;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \Magento\Framework\Registry $coreRegistry
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \Magento\Framework\Registry $coreRegistry,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory
    ) {
        $this->resultPageFactory = $resultPageFactory;
        parent::__construct($context, $coreRegistry);
    }

    /**
     * Edit item
     *
     * @return \Magento\Framework\Controller\ResultInterface
     * @SuppressWarnings(PHPMD.NPathComplexity)
     */
    public function execute()
    {
        // 1. Get ID and create model
        $id = $this->getRequest()->getParam('test_id');
        $model = $this->_objectManager->create('Aion\Test\Model\Test');

        // 2. Initial checking
        if ($id) {
            $model->load($id);
            if (!$model->getId()) {
                $this->messageManager->addError(__('This item no longer exists.'));
                /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
                $resultRedirect = $this->resultRedirectFactory->create();
                return $resultRedirect->setPath('*/*/');
            }
        }
        // 3. Set entered data if was error when we do save
        $data = $this->_objectManager->get('Magento\Backend\Model\Session')->getFormData(true);
        if (!empty($data)) {
            $model->setData($data);
        }

        // 4. Register model to use later in blocks
        $this->_coreRegistry->register('test_item', $model);

        /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
        $resultPage = $this->resultPageFactory->create();

        // 5. Build edit form
        $this->initPage($resultPage)->addBreadcrumb(
            $id ? __('Edit Item') : __('New Item'),
            $id ? __('Edit Item') : __('New Item')
        );
        $resultPage->getConfig()->getTitle()->prepend(__('Items'));
        $resultPage->getConfig()->getTitle()->prepend($model->getId() ? $model->getName() : __('New Item'));
        return $resultPage;
    }
}

As a first step, the edit action(execute() function) makes a query for the test_id parameter. Then it initializes the Aion/Test/Model/Test model class. If the test_id parameter has a value, it attempts to load the model with the id just described. In case of a failure, we get an error message and we are redirected. In case of success, it stores the loaded model in the registry ($this->_coreRegistry->register(’test_item’, $model)). This is called and used by the form container class, mentioned above, from the registry. Finally, it creates the page ($resultPage), and then it sets the title parameter and breadcrumb for the page. We create the layout file belonging to the controller in the test_test_edit.xml file located in the Aion/Test/view/adminhtml/layout/ directory:

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="editor"/>
    <body>
        <referenceContainer name="content">
            <block class="Aion\Test\Block\Adminhtml\Test\Edit" name="test_test_edit"/>
        </referenceContainer>
    </body>
</page>

Next, we set up the saving process. We create the class in the Save.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

class Save extends \Aion\Test\Controller\Adminhtml\Test
{
    /**
     * Save action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
        $resultRedirect = $this->resultRedirectFactory->create();
        // check if data sent
        $data = $this->getRequest()->getPostValue();
        if ($data) {
            $id = $this->getRequest()->getParam('test_id');
            $model = $this->_objectManager->create('Aion\Test\Model\Test')->load($id);
            if (!$model->getId() && $id) {
                $this->messageManager->addError(__('This item no longer exists.'));
                return $resultRedirect->setPath('*/*/');
            }

            // init model and set data

            $model->setData($data);

            // try to save it
            try {
                // save the data
                $model->save();
                // display success message
                $this->messageManager->addSuccess(__('You saved the item.'));
                // clear previously saved data from session
                $this->_objectManager->get('Magento\Backend\Model\Session')->setFormData(false);

                // check if 'Save and Continue'
                if ($this->getRequest()->getParam('back')) {
                    return $resultRedirect->setPath('*/*/edit', ['test_id' => $model->getId()]);
                }
                // go to grid
                return $resultRedirect->setPath('*/*/');
            } catch (\Exception $e) {
                // display error message
                $this->messageManager->addError($e->getMessage());
                // save data in session
                $this->_objectManager->get('Magento\Backend\Model\Session')->setFormData($data);
                // redirect to edit form
                return $resultRedirect->setPath('*/*/edit', ['test_id' => $this->getRequest()->getParam('test_id')]);
            }
        }
        return $resultRedirect->setPath('*/*/');
    }
}

In the first step, the controller class receives the data “posted” by the form created earlier ($data = $this->getRequest()->getPostValue();). If it is not an empty array, it initializes the Aion/Test/Model/Test model class, and if the test_id exists, received as a parameter (i.e. we are not saving a new object), then it loads it with the corresponding id. Then it sets the data received in the post and saves the model. When we have finished with this, we can add new objects from the admin grid, created earlier, and then save and edit these as well. One more significant controller needs to be created, which is responsible for deletion. We create the class in the Delete.php file located in the Aion/Test/Controller/Adminhtml/Test/ directory:

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Adminhtml\Test;

class Delete extends \Aion\Test\Controller\Adminhtml\Test
{
    /**
     * Delete action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */
        $resultRedirect = $this->resultRedirectFactory->create();
        // check if we know what should be deleted
        $id = $this->getRequest()->getParam('test_id');
        if ($id) {
            try {
                // init model and delete
                $model = $this->_objectManager->create('Aion\Test\Model\Test');
                $model->load($id);
                $model->delete();
                // display success message
                $this->messageManager->addSuccess(__('You deleted the item.'));
                // go to grid
                return $resultRedirect->setPath('*/*/');
            } catch (\Exception $e) {
                // display error message
                $this->messageManager->addError($e->getMessage());
                // go back to edit form
                return $resultRedirect->setPath('*/*/edit', ['test_id' => $id]);
            }
        }
        // display error message
        $this->messageManager->addError(__('We can\'t find the item to delete.'));
        // go to grid
        return $resultRedirect->setPath('*/*/');
    }
}

The delete action (execute() function) makes a query for the test_id parameter first. Then it initializes the Aion/Test/Model/Test model class. If the test_id parameter has a value, it attempts to load the model with the aforementioned id and then it executes deletion.   I really hope that I have managed to describe thoroughly how you can create your own module in the Magento 2 system and also how you can set and edit different items or files belonging to it, e.g. database tables, models, collections, blocks, admin grids, layout etc.

You can read Part 1 of this article here: Magento 2 module development – A comprehensive guide – Part 1

See also Part 3 (observers) and Part 4 (Knockout JS).

 

 

What you really need to know about ecommerce blogging

We will convince you with the numbers

Anyway, how much is it worth blogging? What can you achieve with uploading some lengthy text on a site where there should not be any – since the aim is to lead the arriving visitors to the products, make them convert and not to make them read! All right, if you are even a bit familiar with online marketing, you may know that one of the best tools of attracting new visitors is exactly the content. Where direct advertisements fail, content can make you reach a whole lot of new people.

 

Visitors

Usually 80% of the visitors arriving at the blog are there for the first time – which means that you get a great tool to capture those who do not yet know you. Gaining new customers is always more difficult (and more costly) than keeping the existing ones. By providing valuable content to them you have already made the first step.

ecommerce blog visitors

It is not a negligible fact either, that a company site which has a blog attracts 55% more of visitors. The users are looking for information, interesting content captures their attention, they are glad to read the texts that are relevant to them.

 

Links

How does a blog influence link building? Incredibly effectively. Companies that blog regularly can generate, on average, 97% more inbound links than their not blogging competitors. It is even better if you make videos and share them in the company blog, because the posts that contain videos generate three times more links than the ones that do not contain any. It is also efficient from the point of view of SEO that the more posts you publish the more easily you can be found. If you publish quality content that the users will read and share, more and more of your sites will rank high on search results pages of the search engines.

 

Leads

But how will all this make you money? Well, according to the statistics, a B2B company that writes a blog generates 67% more leads than a company that does not blog.

incoming business leads

Two thirds of online marketing professionals list blogs among the five most efficient lead generating tools and 68% of them planned in 2015 to intensify their blogging activities. Have we mentioned yet that the marketing professionals who prioritize blogging are 13 times more likely to benefit from positive ROI?

 

What and how to write?

The advantage of a company blog in ecommerce is priceless: you gain your own channel with it through which you can communicate the most important industrial news, can build your brand, can answer frequently asked questions, where you can write relevant and useful texts about your products (which may convince many customers that it is worth spending some money) without wanting to “pushily” sell anything. You can also use it as a kind of customer service platform: you may post here positive opinions, testimonials that will help you look more trustworthy. You can handle claims, complaints publicly, creating sympathy with the transparency and at the same time blowing away certain doubts of new potential clients concerning the company or the product. You will not just simply communicate with the customers with the help of the blog. If you write in an interesting way, you can build an audience that will come back even only to read your content – regardless of the fact whether they want to actually purchase something from you.

 

How to find the right topics…

Before you start blogging, it is worth surveying your target audience. First of all, take a look at what questions, expressions, keywords your audience looks for using the search engines – thus you will immediately find a great deal of topics that may be worth processing.

write down blogpost ideas

Find the relevant groups in the social media and monitor what the users communicate about. In what subjects do they ask for help in the forums? Which are the questions that arise relatively frequently? You can make thoroughly elaborated, detailed posts to answer these, which will not only strengthen you from an SEO point-of-view but by giving relevant and valuable answers, your expert status will also be strengthened in the eyes of your target audience.

You may find excellent topics if you review pages that are built specifically on a questions and answers format, for example, many professionals worldwide visit the high quality Quora first. Ideally, you will never run out of topics, but you can make the process simpler. Never search for only one topic at a time but for one or two dozens, in order to prepare well in advance. This way you will have more time to collect sources, gather information in order to make fully fledged posts. Use a publication calendar: define at least two weeks in advance when and what topic you will elaborate and when your posts will be published in your blog.

 

Style, tone

It is up to you what style you will use to communicate – whether you want to build a human interest, friendly brand or would you rather show a more distant, impersonal,  severely expert image? Define this and stick to it even in the long run. Do not change your style in each post: the aim is to create a consistent image of yourself that those who regularly read you will get used to and will recognize. It is also important that the style is in line with the images you transmit through other channels. Try to communicate similarly in e-mail, in social media and also in your blog.

 

About writing

It is not very easy to write well. Many companies rather employ professional writers to write their blog because passion does not always make up for the several thousands of hours of experience.

about blogging

At the same time, you can elaborate effective and enjoyable texts yourself by respecting some simple principles. In some cases, it may specifically be an advantage if you do not outsource writing – in case of customer relationship type posts, complaint handling or in case of the very thorough presentation of products it may be useful if the one who writes the blog gained the experience first hand.

Make sure you edit a text that is enjoyable: do not use too large paragraphs, start new ones after each couple of lines. Use lots and lots of subheadings that are short and raise attention.

The function of these is to make those readers that only run through the text easily find what they are interested in. For example, if you are writing about a given product type, these people will most likely be interested in one or two particular issues, so it is worth editing your text in a way that they already notice these even during a quick scrolling. Always highlight the main messages and it will not do harm either if you use illustrations.

Furthermore, you can also use your previously made videos: some user groups prefer watching an informative video of a couple of minutes to reading a longer professional type text (e.g. managing directors). That is why it is also important to…

 

Mutate the content!

Once you write a blog, also use your different pieces of content on other platforms and in different forms. You can make an infographic from an extremely well written professional article, you can edit a downloadable e-book by compiling your writings of similar subjects. You can work several times and in different forms on a topic loved by the audience. Moreover, that is indispensable.

Even the best blogs repeat themselves and that is for a reason: you can regard it as a kind of A/B test where you target the different buyer personas with different content or you can try to achieve bigger success with the same audience.

 

Distribute your content wherever you can

Also make sure you share each and every one of your articles on as many platforms as possible. A Facebook page for example is ideal for that: you can publish your best articles again and again from time to time. You can find out with a little experimenting which are the best channels for you – it may also happen that the best social platform for you will not be Facebook but LinkedIn or Instagram for that matter.

Ecommerce blogging content distribution

Do not publish only on your own sites: share your posts also with different groups and in different forums where your target audience will find them in an organic way.

It is also worth sending out automated newsletters. Offer a bait (for example a free e-book), you only request an e-mail address in return from the user. This way you can send the users a couple of times a month the most popular posts or the ones that are the most relevant to them – and of course you can recommend your products to them.

 

You don’t need to work alone

Your blog will attract a much bigger audience if you can somehow have an influence on the online opinion leaders. These may be authors who write consumer blogs, recognised experts in your professional field or online celebrities who lead activities that are relevant to your online store in some way.

It is best when they also share and link your articles. Consequently, this results in you becoming much more visible than if you shared the content only through your own channels. You are now on your way to going viral. However, you can also contact them in a more direct way. You can invite them to write articles for your blog and you can also send guest posts to other popular blogs yourself.

All this will also bring you traffic for free while assisting you in building your link profile, thus strengthening you in terms of SEO.

 

You have to wait for success but it is worth it!

Blogging is a strategy that will not bring you results in a couple of weeks. This is a long-term investment that, on the other hand, will definitely return if you pay attention to a few little things. It may also happen that you will realize what your blog is really about only after a year.

It is also possible that you will get started with the intention of writing about a certain subject, but it may turn out as time passes that your audience is interested in something totally different. You may even find out that your audience is different than what you originally thought. The blog has to become mature, you have to collect sufficient experience and mainly data to be able to say anything certain about it.

After a year you will know exactly which topics and which content formats the individuals in your audience like. You will also know which posts generate leads and which only bring you visitors or in what style you have to communicate. You will be stronger by that time, because you will have returning visitors, a reader base, new inbound links will reinforce your profile and you will be known throughout the social media.

If you are eager to calculate the returns on investment only after a couple of weeks, you might as well forget about blogging. This is a strategy that rewards those who are patient.

Ecommerce blogging write ideas

10 tips for effective blogging

  1. Make a content calendar! But be flexible as well: Do not be afraid to change your plans if you have to write about current events, or if the preferences of your audience change. Dare to modify your plans according to the data.
  2. Define the efficiency indicators! You have to know what to measure. It is not irrelevant whether the number of visitors or leads, the ratio of successful conversions or the links pointing to your website represent success for you. You have to explicitly lay down KPIs already at the beginning.
  3. Use analytics and analyse! Check at regular intervals which are those posts, formats and date and time that bring better results and focus on those!
  4. Write down your ideas! The best topic ideas come to your mind randomly, so make sure you collect them in one place and preferably do not note them on a bunch of little scraps of paper.
  5. Have you run out of topics? Absolutely not. Look around on professional pages and in relevant groups of the social media. Use BuzzSumo in order to find out which are the most popular relevant topics that you can process.
  6. Harmonize your blog, social and e-mail strategies! Transmit your contents through all possible channels in order to reach as many people as possible.
  7. Make it easily accessible! The user should be able to easily reach the main page of the blog, with one click from the opening page or from any other page of the ecommerce store, and also the relevant content from the store’s sub-pages.
  8. Do not forget about SEO! Never write for Google, but always keep in mind to look for topics that people actually search on. Carry out keyword analyses, review what kind of actual long tail expressions your buyer personas enter in the search engines and answer their questions.
  9. Do not be a lonely hero! If you want to do everything alone, including the writing, the analytics, the design, the distribution, the planning and the organization, your back will break under this task. Entrust your capable colleagues if you have to, hire professionals if necessary.

 

Don’t give up! Be patient and produce more and more quality material. Trust your efforts: if you build it, they will come.

 

 

How to set up Selenium automated tests for Magento projects

After reading this article you will find out:

  • Why to choose Selenium?
  • When is it especially worth using it?
  • How is Selenium structured and what is it made up of?
  • How to download it?
  • What are its functions? How to use it?
  • At the end of the article I will show you the process through a simple example.

Come on, join me!

magento selenium tester bea

 

An ecommerce entrepreneur must follow the changes of the market in today’s competitive retail industry and he or she has to make regular modifications to the website. These smaller changes make it possible for the online store owner to react on the latest trends and keep up with competitors. These modifications may be new products, offers, discounts, shipping methods, payment methods and many other things. The companies must ensure “flawless” user experience even during the regular modifications, otherwise these may lead to negative and undesirable results. For this purpose the testers must continuously check the website and remove the errors. Web developer companies work according to an agile methodology where the “sprints” are quite short and it is necessary to run comprehensive regression tests after each modification. Unfortunately, when changes are made too often, manual testing may become monotone and may take a lot of time. Sooner or later you will have to apply some sort of “facilitation”. This is where Selenium comes into the picture. Let’s see why.

 

1) Why Selenium?

 

SELENIUM:

Selenium is a Firefox extension that makes Black Box type user interface testing possible, which means that you test the application as a whole without knowing its internal operation. You can record test cases with the Selenium IDE Firefox plugin that automatically records the operations done in the browser.

 

This may very much facilitate the life of testers because the tests are developed quickly, but still in an optimal way, and this can be quite cost effective. In addition, Selenium IDE is a remarkably helpful tool since it makes the steps after which the application becomes testable effectively, quickly and in a robotic way, thus putting the software in a condition that would be boring and tiring to reach or test manually. If you automate the path, and following it to reach this point, you will no longer be exhausted and stressed out and you will still be able to provide the best of your testing knowledge.

 

Introduction of automated tests has countless advantages. Let us check out some:

  • It is possible to repeatedly replay the test cases
  • You may perform test cases simultaneously
  • You can run tests without supervision
  • It increases accuracy and greatly reduces errors generated by people
  • You can save time and money

This is where some important questions arise:

  • What is the best tool to automate my test cases?
  • What costs are implied?
  • How easy is it to apply?

In case of web applications, as I’ve already mentioned, Selenium is a very good solution to all issues mentioned above. Why?

  • It is easy to use
  • It is free (open source)
  • It has a great user database and an assisting community
  • It supports several programming languages

 

2) When is it especially worth using Selenium?

Selenium IDE is an easy-to-use tool for simple test cases but in case of the more complex ones (where, for example, there are several branches/forks), it is less easy to apply. It is really worth making Selenium tests when there are no major further modifications to the interface, but development of the backend logic, services or background systems is ongoing. When doing Selenium user interface testing, the tests can be replayed any time, so you can continuously be informed about the results of your tests!

 

3) How is Selenium structured and what is it made up of?

Selenium is not just a separate tool that can only be used by itself, but it is rather a package made up of different testing tools. This package consists of the following components:

  • Selenium Integrated Development Environment (IDE) Selenium ide icon
  • Selenium Remote Control (RC) Selenium RC icon
  • Selenium WebDriver
  • Selenium Grid  Selenium Grid icon

The users may choose from these tools according to their specific needs.

Selenium Package

 

Selenium IDE

Selenium IDE is the simplest tool of the Selenium Package. Its Record and Replay functions make learning it exceptionally easy, as it requires none or just a very little programming competence.  

Selenium RC (Selenium Remote Control)

Selenium RC (Selenium 1) is a tool written in Java that allows creation of test scripts to web applications in the desired programming language and it also ensures their running. This tool is not really used by itself any more.

Selenium Grid

Selenium Grid makes a new function available, namely that the tests written in Selenium RC can be run in several browsers and on several platforms simultaneously.  

WebDriver

WebDriver is a separate tool, but it has a lot of advantages especially when it is combined with Selenium RC. The fusion of the two is often called Selenium 2. WebDriver directly reaches the browsers that support automation.  

Supported browsers:

Selenium - Supported browsers

Supported programming languages:

Selenium - supported programming language

Supported operating systems:

Selenium - supported operating systems

 

4) How to download Selenium?

Step 1: Open the browser (Firefox) and enter the seleniumhq.org URL. This is the official site of Selenium. Click on the “Download” tab where you will find all existing versions of Selenium.

 

Selenium download

 

Step 2: Under the Selenium IDE entry you will find a link that will take you to the extensions page of Firefox, from where you can immediately download and install Selenium IDE.

 

Selenium IDE download install

 

Step 3: Click on the “Add to Firefox” button.

 

Selenium IDE Firefox

 

Step 4: Click on the “Install” button.

 

Selenium installation

 

Now the icon of Selenium IDE appears in the upper right-hand corner of your browser. Later you can start the programme from here. If you click on it, the Selenium window will immediately pop up.

 

Selenium install

 

5) Functions and use of Selenium

Let us look at its menus and functions in detail!

 

1) Menu bar

The menu bar is placed in the upper part of the Selenium window and it consists of five parts:

  • File
  • Edit
  • Actions
  • Options
  • Help

1.1. File menu The File menu is very similar to that of any other application. It allows the user to:

  • Create new test cases, open existing ones or save the actual one.
  • Export a given test case or a test suite in the desired programming language with the ”Export Test Case As” and the “Export Test Suite As” options. The first one exports the currently active test cases while the second one exports all that are open.
  • Close the test case.

Selenium menu file

Selenium IDE test cases can be saved in the following format:

  • HTML format

Selenium IDE test cases can be exported in the following formats/programming languages:

  • java (exported in Java)
  • rb (exported in Ruby)
  • py (exported in Python)
  • cs (exported in C#)

1.2. Edit menu

Selenium edit menu

 

The Edit menu allows options like Undo, Redo, Cut, Copy, Paste, Delete and Select all, which are usually present in all Edit menus. In addition to these, the following ones are available:

  • Insert New Command – this allows the user to insert a new command within the test case
  • Insert New Comment – this allows the user to insert a comment within the test case that explains subsequent commands

Insert New Command The new command goes above the selected command/step.

 

Selenium new command

 

The user can now insert the new command by filling in the Target and the Value fields.

 

Selenium new command target value

 

Insert New Comment The new comment is inserted the same way as the new command is.

 

Selenium new comment

 

The colour purple indicates that this is a comment/explanation.

 

1.3 Actions Menu

 

Selenium actions menu

 

You can choose from the following options under Actions menu:  

  • Record – When you click on this button, Selenium IDE starts recording the steps made in the Firefox browser.
  • Play entire test suite – This option runs all test cases belonging to the current test suite.
  • Play current test case – This option runs the currently active selected test case.
  • Pause/Resume – Running of test cases can be paused and resumed.
  • Toggle Breakpoint – The user can insert one or even several breakpoints in the test, thus forcing the test to stop at any step.
  • Set/Clear Start Point – Here you can add a starting point to any step, which the test will start from thereafter.
  • It is also here where you can set the running speed of the test with the “Faster, “Fastest”, “Slower” and “Slowest” buttons.

 

1.4 Options menu

 

Selenium options menu

 

The Options menu allows the user the practise the handling of settings supported by Selenium IDE. The user can “play” with the general settings (General), with the available formats (Formats), the available extensions (Plugins) and with the available locators and their order (Locator Builders).

 

Selenium options menu general

 

1.5 Help menu If you are stuck, this is where you can check out the documentation, this is where you can report eventual detected errors and this is from where you can go to the official website or blog page of Selenium.

 

2) The URL bar

The URL bar looks the same and operates roughly the same way as that in a browser. It stores the addresses of previously visited sites, so later you will only have to select one, which makes your life much easier, especially if you cannot remember and keep in mind all the addresses of the websites you work on.

 

Selenium url bar

 

3) Toolbar

 

Selenium toolbar

 

You can find the toolbar directly under the URL bar and it includes the following options:

  • Playback Speed – This is where the running speed of the test can be set. Selenium toolbar playback speed
  • Play test suite – With this button you can run all test cases that are currently open. Selenium toolbar play test suite
  • Play test case – You can run the currently active test case with this button. Selenium toolbar play test case
  • Pause – You can pause the running of the test. Selenium toolbar pause
  • Step – The user can step over a given command implementing one command at a time. This is generally used at error correction. Selenium toolbar step
  • Rollup – You can unite two or more steps. Selenium toolbar rollup
  • Record – You can start and stop the recording with this button. Selenium toolbar record

 

4) Editor

The editor is the window where the steps of the test case are visible. This is where the newly recorded commands appear, too. The editor has two views:

 

1) Table view

 

Selenium table

 

2) Source view The test case appears in HTML format here.

 

Selenium source view

 

The user has the opportunity to enter the commands in the Editor, Selenium offers the possible options right after entering the first character. You can select the given item directly from the browser if you click on the Select button. The Find button points to the item associated to it based on the given value.

 

Selenium find

 

5) Test Case window

 

Selenium test case window

 

This is where the latest test case will appear, but since it is possible in Selenium to open several test cases at the same time, all of them will be listed here. This way you can easily choose which one you would like to work on for the moment. When clicking on a test, its steps appear in the Editor window. Test results also appear here, which Selenium indicates in two colours: red and green.

  • Colour Red indicates errors and the failure of the test
  • Colour Green can be associated with a successful test case

 

6) Log window

 

Selenium log window

 

On the Log tab you can see information, in the form of messages on each step separately, at the moment when they are run. It also appears here, indicated in colour red, if there are errors in the test or if it failed. This way you can easily make corrections to your test later on. You can also find the Reference tab here where you get detailed description of a given command, and also the Expert tab that is added to Selenium by an extension. Hints and correction tips appear here that can be immediately performed by clicking on the Inspect and then the Fix buttons. With this, we have more or less arrived at the end of the introduction and the theoretical part. I would like to finish my article with a short example so that you can see all that has been mentioned above in practice, too. This is really a simple test case, much more complex ones could be made up, but I did not want to overcomplicate things since that is not the point of my article.

 

6) ‒ Example

Making a test “script” consists of 3 major steps:

  1. Record – Selenium IDE records all clicks and operations done in the browser.
  2. Replay – The already made up test case has to be replayed several times in order to make sure it works well, whether it needs eventual correction or fine-tuning.
  3. Save – In case the test is reliably stable, it is recommended to save it, so that it can be used again any time in the future.

 

Record

Step 1 Launch Firefox browser and also Selenium at the same time by clicking on the icon in the toolbar.   Step 2 Enter the address of the website to be tested (accounts.google.com) in the URL bar.

 

Selenium URL testing

 

Step 3 The Record button is enabled by default, in case it is not, do not forget to enable it, otherwise it will not record the steps.   Step 4 Open the given website (accounts.google.com) in Firefox.

 

Selenium google sign-in example

Step 5 Enter an existing and real e-mail address.   Step 6 Enter the password that belongs to the e-mail address.   Step 7 Click on the “Sign In” button to complete signing in.   Step 8 At the end, you can stop the recording with the red button and can replay your test case.   WARNING. In case you have not recorded the Sign out step in the test case, you need to make this step by all means before replay, otherwise Selenium will not find the necessary fields or buttons.

 

Replay

Now that your test is done, you need to replay it in order to see whether it is stable enough. Click on the “Play current test case” button.

 

Selenium replay

Save

In case everything is fine, you can save the script by clicking on the File -> Save as option.

 

SUMMARY

It is an obvious objective to improve the quality of your website and to offer a positive user experience. Still, the people working to reach this aim, must love and enjoy their work in the meantime in order to give their best. All in all, Selenium is a very useful tool with which you can facilitate your testers’ job and provide better quality. As explained above, it is relatively easy to use, but requires a lot of practice, since there is a great number of possibilities and options if more complex tasks are to be performed.

 

 

Magento 2 module development – A comprehensive guide – Part 1

This article will discuss the following topics:

 

1) Requirements, documents, installation

The most significant document sources for Magento 2.0 can be reached at the following two links: http://devdocs.magento.com/ https://github.com/magento/magento2 Before installing Magento 2.0, you need to provide the minimal settings of the development environment to be able to install the system. You can find detailed information here: http://devdocs.magento.com/guides/v2.0/install-gde/bk-install-guide.html The basic requirements can be seen right at the start.

  • PHP 5.5.x and greater versions
  • MySQL 5.6.x and greater versions

If you do not currently have these, you need to update your local development environment before installation.

You can use three methods for installation.

1. In the first, you simply download the two available versions from the official Magento website. One of them includes only the basic system, the other provides some Sample Data to the system during the installation process.   The two packages are available here: https://www.magentocommerce.com/download

2. According to the second method, you clone the Magento system from Github. It has the advantage that you can always update the system with Git in your development environment. It is important to mention that you get the develop branch while doing the Git cloning, so after the cloning has been finished, it is worth switching over (git checkout) to 2.0 or merchant beta branches.   Magento 2.0 Github is available at: https://github.com/magento/magento2

3. The third option is Metapackage installation. Here you install Magento 2.0 with the help of the composer after downloading the package. This means that you will get the core Magento extensions from the official Magento Repository and other “components” from another repository. Metapackage installation guide: devdocs magento integrator install

 

2) Appropriate IDE settings before starting developments

After you have successfully installed Magento 2.0 in your development environment and it functions properly, it is recommended to set the IDE to be used for developments. In this article it refers to Jetbrains PhpStorm. Magento 2.0 applies the following: devdocs magento tech stack

First, after opening the project, it is recommended to run the Detect PSR-0 Namspace Roots command in PhpStorm, which can be found in the menu, but IDE will show it automatically anyway. The next step is setting the appropriate PHP CodeSniffer. By default, IDE supports PSR-1 and PSR-2 coding standards, so choosing PSR-2 is a good idea, but I recommend using PSR-4 (note, it is not in PhpStorm by default).

I’m not writing about making these settings in IDE. If you open an etc/module.xml file belonging to any extension in the vendor/magento directory, you can see that URNs are not resolved. Magento 2.0 has a good number of development commands that can be helpful throughout the development process. You can see the list of these commands the following way:

  • enter the bin directory in your terminal
  • give the “php magento” command
  • as a result, all the commands are listed that can be used in the development process

You can solve the URN resolve problem with the following command: php bin/magento dev:urn-catalog:generate .idea/misc.xml (you can see that it is not run from the bin directory, but from the root directory) Before the development starts, switch off the whole cache (in the admin area or with the help of the php Magento commands mentioned above).

If you make your developments using Apache servers, you should set the developer mode as SetEnv MAGE_MODE developer in the .htaccess file located in the root directory. It is also recommended to switch off xdebug (php.ini setting) in your development environment so that processes can run much faster.

[bctt tweet=”It is recommended to set the IDE properties to be used for Magento 2 developments.” username=”aionhill”]

 

3) Structuring and creating our basic Magento 2.0 extension

After installing Magento 2.0 (not cloned from Github), we immediately realize that the familiar app/code directory is missing and all core Magento modules are found under the vendor/magento directory. Not to worry about it, the first step is to create the app/code directory. After finishing with that, we can create the base directory of our own module (Vendor/Module). In our case, it is Aion/Test directory.   Next, two directories are created:

  • app/code/Aion/Test/etc
  • app/code/Aion/Test/Helper

The module’s base definition file is placed in the app/code/Aion/Test/etc, under the name module.xml

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Aion_Test" setup_version="2.0.0">
        <sequence>
            <module name="Magento_Store"/>
        </sequence>
    </module>
</config>

We define the name of the module in the file (Vendor_Module -> Aion_Test), the version number (2.0.0 in our case) and the dependencies, which the core Magento_Store module. In the next step, we create the Helper file. Our helper file is this: app/code/Aion/Test/Helper/Data.php. Let’s see what it contains:

<?php
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Helper;

/**
 * Aion Test helper
 */
class Data extends \Magento\Framework\App\Helper\AbstractHelper
{
    /**
     * Path to store config if extension is enabled
     *
     * @var string
     */
    const XML_PATH_ENABLED = 'aion/basic/enabled';

    /**
     * Check if extension enabled
     *
     * @return string|null
     */
    public function isEnabled()
    {
        return $this->scopeConfig->isSetFlag(
            self::XML_PATH_ENABLED,
            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
        );
    }
}

 

It is enough to create an “empty” helper file, however, we have already implemented our first function (public function is Enabled()) and a constant value. The string defined in the constant value will get its meaning later in the extension’s admin configuration panel (system.xml). Next, we insert the registration.php file in the module directory (app/code/Aion/Test), which is used for “recognizing” the module within the Magento 2.0 system. Let’s see what it includes:

 

<?php
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Aion_Test',
    __DIR__
); 

 

We can copy and insert the file’s content from any of the core Magento 2.0 modules, the point is to have our own module at the second parameter (Vendor_Modul -> Aion_Test). Since Composer is used by the core Magento 2.0 modules for updates, we too create our own composer.json file. It includes the following:

 

{
    "name": "aion/module-test",
    "description": "N/A",
    "require": {
        "php": "~5.5.0|~5.6.0|~7.0.0",
        "magento/module-config": "100.0.*",
        "magento/module-store": "100.0.*",
        "magento/module-backend": "100.0.*",
        "magento/framework": "100.0.*"
    },
    "type": "magento2-module",
    "version": "100.0.2",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "Aion\\Test\\": ""
        }
    }
}

 

The composer.json includes the basic information of our module, e.g. dependencies, licence, version data.

Almost done, but there are still two files missing for a nicely built-up module. These two files are located in the module’s directory (app/code/Aion/Test): README.md and COPYING.txt.

The first one is a Git description, the second one contains the licence information, which is OSL 3.0 in our case. So our module structure looks like this: app/code

Aion/

Test/

etc/module.xml

Helper/Data.php

composer.json

COPYING.txt

README.md

registration.php

 

Next, we need to activate the module and check if Magento 2.0 has recognized it appropriately. In the Magento 1.x system this could be carried out by updating the frontend or admin page, however, with version 2.0, we need to apply the commands already described earlier.

Now enter the /bin directory in the terminal and execute the php magento module:status command. If everything has been done correctly, then the Enabled modules list and the Disabled modules list (inactive or not yet enabled) will be listed. In the latter we will find our own module: Aion_Test. If our own module is not displayed, some mistake has been made.

Next, we execute the php magento module:enable Aion_Test command in the /bin directory in the terminal. Now we can see that the module is recognized and approved (enabled) by the system. This means that ’Aion_Test’ => 1 entry has been placed in the array within the app/etc/config.php file. Then you get a notification to “register” the module on a database level as well.

So you give the php magento setup:upgrade command in the terminal. During the command execution, Magento 2.0 runs through all the core modules and custom modules and updates the system, i.e. runs the database schematics and data scripts if it finds a higher version number than the current one. In the case of our module it means that the 2.0.0 version number, as described earlier, will be entered in the setup_module table.

Now, if we log in to the admin area and select the Store / Configuration menu and then Advanced / Advanced page, then our module is displayed in the list in an “enabled” stage. So our basic module is finished.

 

4) Managing module admin configuration and user roles

After our basic module is finished, we should create its admin configuration and user roles settings. First, we create the file needed for configuration, which will be located in the app/code/Aion/Test/etc/adminhtml directory under the name system.xml.

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="aion_tab" translate="label" sortOrder="500">
            <label>Aion Extensions</label>
        </tab>
        <section id="aion" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Test Extension</label>
            <tab>aion_tab</tab>
            <resource>Aion_Test::test</resource>
            <group id="basic" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>Basic</label>
                <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enable Extension</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
            <group id="more" translate="label" type="text" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>More</label>
                <field id="variable" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Some Variable</label>
                </field>
            </group>
        </section>
    </system>
</config>

 

We define a <tab> tag in the file, thus our module appears under a separate menu (Aion Extensions -> Test Extension) on the Stores / Configuration page in the admin area. We implement two tabs here (with basic and more IDs). In the first we place the basic No / Yes select type configuration variable, which we use for enabling or disabling the module. Its query has been implemented earlier in the Data.php file (Helper/Data.php). For the sake of an example, we create a text type configuration variable named ‘Variable’ for later use.

Additionally, an important element in the file is the <resource> tag, which will be used with user roles management implementation. In our case it is Aion_Test::test.

Next we create the file necessary for user roles management, which will be located in the app/code/Aion/Test/etc directory under the name acl.xml. This contains the following:

 

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="Aion_Test::test" title="Aion Test Extension Section" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

 

Having finished with this, we check if the user roles settings of the module have been created properly in the Role Resource (Admin user roles) section. Open the System / User Roles menu and then select any of the administrative groups (there is one by default) in which select Role Resources. Then under Custom, select Resource Access and check if you can find your own module in the role resources tree (hierarchy).

Next, we make sure if the configuration menu and its content, created earlier, appears. In the admin area we select Stores / Configuration, on the left, and check if our own menu (Aion Extensions) and its content (Test Extension) are displayed.

We define the basic configuration values in the config.xml file in the app/code/Aion/Test/etc directory:

 

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <aion>
            <basic>
                <enabled>1</enabled> 
            </basic>
        </aion>
    </default>
</config>

It is clearly visible that the tags seen in the file correspond to the IDs in the system.xml.

 

5) Basic frontend controller, block, layout and template

Now we will create our own frontend controller together with the corresponding layout and router configuration. And then we implement a template file and a block belonging to it.

First, we create the controller file, which is an Action in reality. We implement this with the Index.php located in the app/code/Aion/Test/Controller/Index/ directory. This file includes the following:

 

<?php
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Controller\Index;

use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\Controller\Result\ForwardFactory;
use Magento\Framework\Exception\NotFoundException;
use Magento\Framework\App\RequestInterface;

/**
 * Contact index controller
 */
class Index extends \Magento\Framework\App\Action\Action
{
    /**
     * @var PageFactory
     */
    protected $resultPageFactory;

    /**
     * @var ForwardFactory
     */
    protected $resultForwardFactory;

    /**
     * @var \Aion\Test\Helper\Data
     */
    protected $helper;

    /**
     * Index constructor.
     *
     * @param \Magento\Framework\App\Action\Context $context
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     * @param \Magento\Framework\Controller\Result\ForwardFactory $resultForwardFactory
     * @param \Aion\Test\Helper\Data $helper
     */
    public function __construct(
        Context $context,
        PageFactory $resultPageFactory,
        ForwardFactory $resultForwardFactory,
        \Aion\Test\Helper\Data $helper
    ) {
        $this->resultPageFactory = $resultPageFactory;
        $this->resultForwardFactory = $resultForwardFactory;
        $this->helper = $helper;
        parent::__construct($context);
    }

    /**
     * Dispatch request
     *
     * @param RequestInterface $request
     * @return \Magento\Framework\App\ResponseInterface
     * @throws \Magento\Framework\Exception\NotFoundException
     */
    public function dispatch(RequestInterface $request)
    {
        if (!$this->helper->isEnabled()) {
            throw new NotFoundException(__('Page not found.'));
        }
        return parent::dispatch($request);
    }

    /**
     * Aion Test Page
     *
     * @return \Magento\Framework\View\Result\Page
     */
    public function execute()
    {
        /** @var \Magento\Framework\View\Result\Page $resultPage */
        $resultPage = $this->resultPageFactory->create();
        $resultPage->getConfig()->getTitle()->set(__('Aion Test Page'));
        if (!$resultPage) {
            $resultForward = $this->resultForwardFactory->create();
            return $resultForward->forward('noroute');
        }
        return $resultPage;
    }
} 

 

There are three crucial functions in the file, all of them are defined in the parent class. We use the __construct function for injecting the other classes we want to use (e.g. helper class, dependency injection). The dispatch function will run automatically after __construct. Here we check if our module is enabled or not and then we manage it accordingly.

Finally, the execute function means the action itself, which is Index action in this case. While in operation, we create the $resultPageFactory object (which will build up the page based on the layout configuration, to be executed later). We define a page title with the object’s setConfig function. Next we check if the page has been created or not. If no error has occurred, we return with the $resultPageFactory object. Otherwise, we redirect the action to a noroute (i.e. 404) page.

 

We create the routers.xml file in the app/code/Aion/Test/etc/frontend directory in which we define which URL we want to use to call the controllers for our module. The file contains the following:

 

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="test" frontName="test">
            <module name="Aion_Test" />
        </route>
    </router>
</config>

It can be seen in the router configuration that we assigned “test” URL to our module. So in this case the {{base_url}}test/index/index will call the controller (more precisely, the execute function  – Index action – within it), detailed above. Naturally, it is not necessary to define the index controller and action in the URL. {{base_url}}test/ will redirect to the same location.

Next we create the layout file app/code/Aion/Test/view/frontend/layout under the name test_index_index.xml. It is obvious that the file name follows the router -> controller -> action names. The file contains the following:

 

<?xml version="1.0"?>
<!--
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <head>
        <title>Aion Test Page</title>
    </head>
    <body>
        <referenceContainer name="content">
            <block class="Aion\Test\Block\Test" name="testPage" template="Aion_Test::test.phtml" />
        </referenceContainer>
    </body>
</page>

 

We define the basic title of the page in the <head> section of the layout file and we also define a block and a template file assigned to it in the content section. We create the block in the app/code/Aion/Test/Block directory in the Test.php. The file includes the following:

 

<?php
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Block;

use Magento\Framework\View\Element\Template;

/**
 * Aion Test Page block
 */
class Test extends Template
{
    /**
     * @param Template\Context $context
     * @param array $data
     */
    public function __construct(Template\Context $context, array $data = [])
    {
        parent::__construct($context, $data);
    }

    /**
     * Test function
     *
     * @return string
     */
    public function getTest()
    {
        return 'This is a test function for some logic...';
    }
}  

 

We can notice that we have not defined anything in the __construct function presented in the example, so this can be omitted. However, it is recommended to create it at the beginning if we want to implement a storeManager, Helper or other elements later on.

The template file is to be located in the app/code/Aion/Test/view/frontend/templates directory under the name test.phtml. The file contains the following:

 

<?php
/**
 * Copyright © 2015 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
?>

<?php
/**
 * @var $block \Aion\Test\Block\Test
 */
?>

<p><?php echo $block->getTest(); ?></p>
<p><?php echo __('This is a text.') ?></p>

 

In this example the template file calls the test function defined in the Block class and also displays a string, which is embedded (inserted) in a translate function.

When all is finished, our module looks like this:

app/code

Aion/

Test/

Block/Test.php

Controller/Index/Index.php

etc/adminhtml/system.xml

/frontend/routes.xml

acl.xml

config.xml

module.xml

Helper/Data.php

view/frontend

/layout/test_index_index.xml

/templates/test.phtml

composer.json

COPYING.txt

README.md

registration.php

 

So far we have seen how to create and build up a basic Magento 2.0 module. In the following, we will see how to create a database table and how to add basic data and models to it that are needed to manage table data and also how to add a collection to it. Furthermore, we will check if these data are “comprehensible” in terms of frontend development and give a method for displaying them.

 

6) Creating the database table belonging to the module – 1

 

We create the database table, belonging to the module, with the help of an installer script already known form Magento 1.x. However, its file name and structure is a little different.

We create the database table in the InstallSchema.php in the app/code/Aion/Test/Setup directory. The file contains the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Aion\Test\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Adapter\AdapterInterface;

/**
 * @codeCoverageIgnore
 */
class InstallSchema implements InstallSchemaInterface
{
    /**
     * Install table
     *
     * @param \Magento\Framework\Setup\SchemaSetupInterface $setup
     * @param \Magento\Framework\Setup\ModuleContextInterface $context
     * @throws \Zend_Db_Exception
     */
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        $installer = $setup;

        $installer->startSetup();
        /**
         * Create table 'aion_test'
         */
        $table = $installer->getConnection()->newTable(
            $installer->getTable('aion_test')
        )->addColumn(
            'test_id',
            \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
            null,
            ['identity' => true, 'nullable' => false, 'primary' => true],
            'Test ID'
        )->addColumn(
            'name',
            \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
            255,
            ['nullable' => false],
            'Test Name'
        )->addColumn(
            'email',
            \Magento\Framework\DB\Ddl\Table::TYPE_TEXT,
            255,
            ['nullable' => false],
            'Test Email'
        )->addColumn(
            'creation_time',
            \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT],
            'Test Creation Time'
        )->addColumn(
            'update_time',
            \Magento\Framework\DB\Ddl\Table::TYPE_TIMESTAMP,
            null,
            ['nullable' => false, 'default' => \Magento\Framework\DB\Ddl\Table::TIMESTAMP_INIT_UPDATE],
            'Test Modification Time'
        )->addColumn(
            'is_active',
            \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
            null,
            ['nullable' => false, 'default' => '1'],
            'Is Test Active'
        )->addIndex(
            $setup->getIdxName(
                $installer->getTable('aion_test'),
                ['name', 'email'],
                AdapterInterface::INDEX_TYPE_FULLTEXT
            ),
            ['name', 'email'],
            ['type' => AdapterInterface::INDEX_TYPE_FULLTEXT]
        )->setComment(
            'Aion Test Table'
        );
        $installer->getConnection()->createTable($table);

        $installer->endSetup();
    }
}

 

The script creates the sample table belonging to the module under the name “aion_test”. Then it creates the following fields:

  • test_id – primary key, smallint (6)
  • name – varchar (255)
  • email – varchar (255)
  • creation_time – timestamp
  • update_time – timestamp
  • is_active – smallint (6)

After that, it is highly recommended to add indexes (addIndex) to text, varchar or other types of columns in which we will make searches or will filter the existing collection. In our example, these two fields are name and email.

 

7) Creating the database table belonging to the module – 2

Previously, we gave a version number to our module with the help of the module.xml located in the app/code/Aion/Test/etc directory, in which we defined the basic version and then with the help of terminal commands for enabling the module, the version number could be included in the “setup module” Magento 2.0 table.

We have two options for running the aforementioned script: either increasing the version number in the module or deleting the module’s earlier version number. Since we are only at the beginning of the development of our module, it is recommended to delete the entry related to our module from the “setup module” Magento 2.0 table. After finishing with this, run again the setup:upgrade magento command from the terminal. If everything has been set appropriately, the table assigned to our module is created.

 

8) Creating the models and collection belonging to the module

In order to load data to the table or make queries for data, we need to create the module’s model, resource and collection files.

We create the basic model file in Test.php in the app/code/Aion/Test/Model directory. This file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Model;

/**
 * Aion Test model
 *
 * @method \Aion\Test\Model\ResourceModel\Test _getResource()
 * @method \Aion\Test\Model\ResourceModel\Test getResource()
 * @method string getId()
 * @method string getName()
 * @method string getEmail()
 * @method setSortOrder()
 * @method int getSortOrder()
 */
class Test extends \Magento\Framework\Model\AbstractModel
{
    /**
     * Statuses
     */
    const STATUS_ENABLED = 1;
    const STATUS_DISABLED = 0;

    /**
     * Aion Test cache tag
     */
    const CACHE_TAG = 'aion_test';

    /**
     * @var string
     */
    protected $_cacheTag = 'aion_test';

    /**
     * Prefix of model events names
     *
     * @var string
     */
    protected $_eventPrefix = 'aion_test';

    /**
     * @return void
     */
    protected function _construct()
    {
        $this->_init('Aion\Test\Model\ResourceModel\Test');
    }

    /**
     * Get identities
     *
     * @return array
     */
    public function getIdentities()
    {
        return [self::CACHE_TAG . '_' . $this->getId(), self::CACHE_TAG . '_' . $this->getId()];
    }

    /**
     * Prepare item's statuses
     *
     * @return array
     */
    public function getAvailableStatuses()
    {
        return [self::STATUS_ENABLED => __('Enabled'), self::STATUS_DISABLED => __('Disabled')];
    }

}

 

We define the two cache tags needed for operating Magento 2.0 cache. It is also important to define an event prefix to use it as part of the event name if using observers later on. The most important thing to do now is defining the resource model belonging to the model in the _construct() function. It is recommended to create the getAvailableStatuses() function and the two STATUS_* constant values for later use.

It is not a must to implement the getIdentities() function, but it still would be useful for the proper operation of cache management.

After we have created our basic model file, we should create the resource model for it. We create the basic resource model file in Test.php in the app/code/Aion/Test/Model/ResourceModel directory. The file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Model\ResourceModel;

/**
 * Aion Test resource model
 */
class Test extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    /**
     * Define main table
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('aion_test', 'test_id');
    }
          
}

 

Now the most important thing is to implement the protected _construct() function, where we define which field represents the primary key in the table created earlier.

Having finished with this, we create the collection with the help of the resource model. We create the basic collection class file in Collection.php in the app/code/Aion/Test/Model/ResourceModel/Test directory. The file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Model\ResourceModel\Test;

/**
 * Aion Test collection
 */
class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
{
    /**
     * @var string
     */
    protected $_idFieldName = 'test_id';

    /**
     * Store manager
     *
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $storeManager;

    /**
     * @param \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
     * @param \Magento\Framework\Event\ManagerInterface $eventManager
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param \Magento\Framework\DB\Adapter\AdapterInterface|null $connection
     * @param \Magento\Framework\Model\ResourceModel\Db\AbstractDb|null $resource
     */
    public function __construct(
        \Magento\Framework\Data\Collection\EntityFactoryInterface $entityFactory,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
        \Magento\Framework\Event\ManagerInterface $eventManager,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Framework\DB\Adapter\AdapterInterface $connection = null,
        \Magento\Framework\Model\ResourceModel\Db\AbstractDb $resource = null
    ) {
        parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource);
        $this->storeManager = $storeManager;
    }

    /**
     * Define resource model
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('Aion\Test\Model\Test', 'Aion\Test\Model\ResourceModel\Test');
    }
}

 

We implement the _construct() function in the collection class, in which we basically define which resource model class belongs to the model class that was created earlier. Essentially, it would be enough and suitable for having a collection, but thinking about the future, it is better to inject the store manager class in the public __construct() function at this moment for later use, because multistore support is mandatory with every module.

After finishing with all three files, we have managed to create those classes with which we can write and read data using our database table created earlier.

 

9) Loading up data to the table using script

 

In order to be able to add data to the module’s basic table using script, we needed to produce the model structure. We create the data script in InstallData.php in the app/code/Aion/Test/Setup directory. The file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Setup;

use Aion\Test\Model\Test;
use Aion\Test\Model\TestFactory;
use Magento\Framework\Setup\InstallDataInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;

/**
 * @codeCoverageIgnore
 */
class InstallData implements InstallDataInterface
{
    /**
     * Test factory
     *
     * @var TestFactory
     */
    private $testFactory;

    /**
     * Init
     *
     * @param TestFactory $testFactory
     */
    public function __construct(TestFactory $testFactory)
    {
        $this->testFactory = $testFactory;
    }

    /**
     * {@inheritdoc}
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
     */
    public function install(ModuleDataSetupInterface $setup, ModuleContextInterface $context)
    {
        $testItems = [
            [
                'name' => 'John Doe',
                'email' => 'john.doe@example.com',
                'is_active' => 1,
            ],
            [
                'name' => 'Jane Doe',
                'email' => 'jane.doe@example.com',
                'is_active' => 0,
            ],

            [
                'name' => 'Steve Test',
                'email' => 'steve.test@example.com',
                'is_active' => 1,
            ],
        ];

        /**
         * Insert default items
         */
        foreach ($testItems as $data) {
            $this->createTest()->setData($data)->save();
        }

        $setup->endSetup();
    }

    /**
     * Create Test item
     *
     * @return Test
     */
    public function createTest()
    {
        return $this->testFactory->create();
    }
}

 

We implement the install(…) function in the InstallData class, in which we create test data in a multi-dimensional array. We iterate through it and then call the createTest() function which has the “task” to create the new Test Model. Then, after adding the bundles as data, we save the model. Here, it is important to mention the TestFactory class which was created in the class and injected in the __construct() function. This class is automatically created by Magento 2.0 in the /var/generation/Aion/Test/Model directory when it is running for the first time ($this->testFactory-> create()).

Since we are still at the beginning of our module development, let’s discard manually the previously created “aion_test” table from the database and delete the entry belonging to our module in the “setup_module” Magento 2.0 table.

Of course, alternatively, we can run data upload in the command line (terminal) using version number upgrade, thus we do not need to discard manually our previously created table and there is no need to deal separately with the “setup_module” table either.

 

10) Table update script and version upgrade

While developing our module, we need to modify frequently the basic database table, sometimes creating a new database table. In the case of Magento 1.x, we could do this with upgrade scripts either with respect to database or to data. It is the same with Magento 2.0, but luckily, now we can manage all the database scripts within one file instead of separate files.

We create the database update script in UpgradeSchema.php in the app/code/Aion/Test/Setup directory. The file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */

namespace Aion\Test\Setup;

use Magento\Framework\Setup\UpgradeSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;

/**
 * @codeCoverageIgnore
 */
class UpgradeSchema implements UpgradeSchemaInterface
{
    /**
     * Upgrades DB schema, add sort_order
     *
     * @param SchemaSetupInterface $setup
     * @param ModuleContextInterface $context
     * @return void
     */
    public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $context)
    {
        if (version_compare($context->getVersion(), '2.0.1') < 0) {
            $setup->startSetup();
            $setup->getConnection()->addColumn(
                $setup->getTable('aion_test'),
                'sort_order',
                [
                    'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT,
                    'length' => null,
                    'nullable' => false,
                    'default' => 0,
                    'comment' => 'Test Sort Order'
                ]
            );
            $setup->endSetup();
        }
    }
}

 

In this example, with the help of upgrade script, we add a new field (sort_order) to the basic database table („aion_test”). The implemented modification in the upgrade function will run only if the module’s version number reaches the 2.0.1 value, seen in the example.

 

11) Frontend data display

In order to display the created data on frontend, we need to modify the frontend template and the block class belonging to it.

The block class has already been prepared previously, now we complement it.

We create the block class in Test.php in the app/code/Aion/Block/ directory. The file contains the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Aion\Test\Block;

use Magento\Framework\View\Element\Template;

/**
 * Aion Test Page block
 */
class Test extends Template
{
    /**
     * @var \Aion\Test\Model\Test
     */
    protected $test;

    /**
     * Test factory
     *
     * @var \Aion\Test\Model\TestFactory
     */
    protected $testFactory;

    /**
     * @var \Aion\Test\Model\ResourceModel\Test\CollectionFactory
     */
    protected $itemCollectionFactory;

    /**
     * @var \Aion\Test\Model\ResourceModel\Test\Collection
     */
    protected $items;

    /**
     * Test constructor.
     *
     * @param \Magento\Framework\View\Element\Template\Context $context
     * @param \Aion\Test\Model\Test $test
     * @param \Aion\Test\Model\TestFactory $testFactory
     * @param array $data
     */
    public function __construct(
        Template\Context $context,
        \Aion\Test\Model\Test $test,
        \Aion\Test\Model\TestFactory $testFactory,
        \Aion\Test\Model\ResourceModel\Test\CollectionFactory $itemCollectionFactory,
        array $data = []
    ) {
        $this->test = $test;
        $this->testFactory = $testFactory;
        $this->itemCollectionFactory = $itemCollectionFactory;
        parent::__construct($context, $data);
    }

    /**
     * Retrieve Test instance
     *
     * @return \Aion\Test\Model\Test
     */
    public function getTestModel()
    {
        if (!$this->hasData('test')) {
            if ($this->getTestId()) {
                /** @var \Aion\Test\Model\Test $test */
                $test = $this->testFactory->create();
                $test->load($this->getTestId());
            } else {
                $test = $this->test;
            }
            $this->setData('test', $test);
        }
        return $this->getData('test');
    }

    /**
     * Get items
     *
     * @return bool|\Aion\Test\Model\ResourceModel\Test\Collection
     */
    public function getItems()
    {
        if (!$this->items) {
            $this->items = $this->itemCollectionFactory->create()->addFieldToSelect(
                '*'
            )->addFieldToFilter(
                'is_active',
                ['eq' => \Aion\Test\Model\Test::STATUS_ENABLED]
            )->setOrder(
                'creation_time',
                'desc'
            );
        }
        return $this->items;
    }

    /**
     * Get Test Id
     *
     * @return int
     */
    public function getTestId()
    {
        return 1;
    }

    /**
     * Return identifiers for produced content
     *
     * @return array
     */
    public function getIdentities()
    {
        return [\Aion\Test\Model\Test::CACHE_TAG . '_' . $this->getTestModel()->getId()];
    }
} 

 

We implement the previously created Test Model and the testFactory belonging to it as well as the collectionFactory (itemCollectionFactory), into the constructor of the block class. The collectionFactory class is very similar to the testFactory, which is generated by Magento 2.0 as well in the var/generation/Aion/Test/Model/ResourceModel/Test/ after its first call. The getTestModel() function creates a model, and is responsible for loading with the appropriate ID, while the getItems() function creates a complete collection.

We can write the data, for testing, in the template file belonging to the block. The file includes the following:

 

<?php
/**
 * Copyright © 2016 AionNext Ltd. All rights reserved.
 * See COPYING.txt for license details.
 */
?>

<?php
/**
 * @var $block \Aion\Test\Block\Test
 */
?>
<div class="aion-test">
    <h2><?php echo __('This is a test extension!') ?></h2>
    <!-- See a sample model -->
    <?php \Zend_Debug::dump($block->getTestModel()->getData()); ?>
    <!-- See a sample collection -->
    <?php \Zend_Debug::dump($block->getItems()->getData()); ?>

    <!-- See a sample collection iteration -->
    <?php $items = $block->getItems(); ?>
    <?php if ($items->getSize()) : ?>
        <?php foreach ($items as $item) : ?>
            <h3><?php echo $block->stripTags($item->getName()) ?></h3>
            <p>
                <span><?php echo __('Email:'); ?></span>&nbsp;
                <span><?php echo $item->getEmail() ?></span>
            </p>
        <?php endforeach; ?>
    <?php endif; ?>
</div>

 

So we now have added our own database table to our basic Magento 2.0 module and loaded it with test data. We have also created the model, resource and collection needed for it and also displayed the test data on frontend.

 

END OF PART 1

In Part 2, we describe how to create the admin grid and the necessary controllers of the module.

See also Part 3 (observers) and Part 4 (Knockout JS).