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

 

 

145 replies
  1. Randall Meharry says:

    I was recommended this blog through my cousin. I am not positive whether or not this put up is written via him as no one else realize such unique about my trouble. You are incredible! Thanks!

  2. Anissa Mercedes says:

    You really make it appear really easy together with your presentation but I in finding this topic to be really one thing which I feel I would never understand. It kind of feels too complicated and very wide for me. I’m taking a look forward to your next post, I?¦ll attempt to get the cling of it!

  3. Substance Abuse Among Nurses says:

    Residential Alcohol Rehab Centers http://aaa-rehab.com Drug Rehab http://aaa-rehab.com Meth Treatment Centers Near Me
    http://aaa-rehab.com

  4. inductive vs deductive says:

    Incredible! This blog looks exactly like my old one! It’s on a totally different subject but it has pretty much the same layout and design. Outstanding choice of colors!

  5. market data says:

    I just couldn’t depart your website before suggesting that I really enjoyed the standard information a person provide for your visitors? Is going to be back often to check up on new posts

  6. where to invest in stocks says:

    Can I simply say what a reduction to seek out somebody who truly knows what theyre talking about on the internet. You undoubtedly know the way to convey a difficulty to light and make it important. Extra people must read this and perceive this side of the story. I cant consider youre not more popular since you positively have the gift.

  7. Popular Media Lies says:

    of course like your website however you have to check the spelling on quite a few of your posts. Several of them are rife with spelling issues and I in finding it very bothersome to inform the truth then again I’ll surely come back again.

  8. religion says:

    Hiya, I am really glad I have found this info. Nowadays bloggers publish just about gossips and internet and this is actually frustrating. A good blog with interesting content, this is what I need. Thanks for keeping this web-site, I’ll be visiting it. Do you do newsletters? Can’t find it.

  9. Proxy Servers says:

    I would love to add that when you do not already have got an insurance policy or perhaps you do not take part in any group insurance, you could possibly well make use of seeking the assistance of a health insurance agent. Self-employed or those with medical conditions usually seek the help of an health insurance brokerage. Thanks for your blog post.

  10. bacurau torrent says:

    Your style is so unique compared to many other people. Thank you for publishing when you have the opportunity,Guess I will just make this bookmarked.2

  11. erjilo pterin says:

    you’re actually a just right webmaster. The website loading pace is incredible. It kind of feels that you are doing any unique trick. Moreover, The contents are masterwork. you’ve performed a magnificent job in this subject!

  12. Joker Online says:

    Hi there! This post couldn’t be written any better! Reading through this post reminds me of my previous room mate! He always kept talking about this. I will forward this article to him. Pretty sure he will have a good read. Thank you for sharing!

  13. SSD OpenVZ VPS says:

    You made some first rate points there. I regarded on the web for the issue and located most people will associate with with your website.

  14. casas de madeira says:

    Thank you for the auspicious writeup. It if truth be told used to be a leisure account it. Glance complicated to far brought agreeable from you! However, how could we keep up a correspondence?

  15. สล็อตXo says:

    Hi, Neat post. There is an issue along with your site in web explorer, would check this?K IE nonetheless is the marketplace leader and a large component of other folks will pass over your excellent writing because of this problem.

  16. all SMO Support says:

    Keep up the great work, I read few posts on this website and I conceive that your web site is rattling interesting and has got circles of great information.

  17. YouTube Video Downloader says:

    I’m not sure where you are getting your information, but good topic. I needs to spend some time learning more or understanding more. Thanks for magnificent info I was looking for this info for my mission.

  18. Cogumelos Shimeji says:

    Thank you for sharing excellent informations. Your web site is very cool. I’m impressed by the details that you have on this website. It reveals how nicely you perceive this subject. Bookmarked this website page, will come back for extra articles. You, my friend, ROCK! I found just the information I already searched all over the place and just couldn’t come across. What an ideal site.

  19. bartcarts says:

    Do you mind if I quote a few of your posts as long as I provide credit and sources back to your webpage? My website is in the very same area of interest as yours and my users would certainly benefit from some of the information you present here. Please let me know if this okay with you. Regards!

  20. original site says:

    I know this if off topic but I’m looking into starting my own weblog and was curious what all is required to get setup? I’m assuming having a blog like yours would cost a pretty penny? I’m not very internet savvy so I’m not 100 certain. Any tips or advice would be greatly appreciated. Kudos

  21. click now says:

    When I originally commented I clicked the -Notify me when new comments are added- checkbox and now each time a comment is added I get four emails with the same comment. Is there any way you can remove me from that service? Thanks!

  22. hearthstone down says:

    It’s in point of fact a nice and helpful piece of information. I’m glad that you shared this helpful info with us. Please stay us up to date like this. Thank you for sharing.

  23. prognatismo says:

    I think other web-site proprietors should take this web site as an model, very clean and excellent user genial style and design, let alone the content. You are an expert in this topic!

  24. prognatismo says:

    As I site possessor I believe the content matter here is rattling magnificent , appreciate it for your hard work. You should keep it up forever! Best of luck.

  25. 1on1 says:

    Very good written information. It will be supportive to anyone who utilizes it, as well as me. Keep up the good work – for sure i will check out more posts.

  26. donanim says:

    I was suggested this website by means of my cousin. I’m now not positive whether or not this post is written by means of him as no one else understand such detailed approximately my problem. You’re wonderful! Thanks!

  27. internet em maranhao says:

    What i do not realize is in fact how you are no longer really a lot more well-favored than you may be now. You’re so intelligent. You realize thus considerably in the case of this matter, made me personally believe it from so many varied angles. Its like men and women don’t seem to be fascinated unless it?¦s one thing to accomplish with Lady gaga! Your individual stuffs excellent. Always deal with it up!

  28. click this link says:

    Very interesting information!Perfect just what I was looking for! “The whole point of getting things done is knowing what to leave undone.” by Lady Reading.

  29. web url says:

    I’m extremely impressed with your writing skills as well as with the layout on your weblog. Is this a paid theme or did you modify it yourself? Either way keep up the nice quality writing, it is rare to see a nice blog like this one these days..

  30. he said says:

    What i don’t realize is actually how you’re no longer really a lot more well-favored than you may be right now. You are very intelligent. You already know therefore considerably on the subject of this subject, produced me personally imagine it from a lot of various angles. Its like women and men don’t seem to be involved unless it’s one thing to accomplish with Lady gaga! Your individual stuffs excellent. All the time deal with it up!

  31. luxury car rental in kl says:

    The very heart of your writing whilst sounding agreeable in the beginning, did not really work properly with me after some time. Somewhere throughout the paragraphs you actually managed to make me a believer unfortunately only for a short while. I however have a problem with your leaps in assumptions and you might do well to help fill in those breaks. When you actually can accomplish that, I could undoubtedly be amazed.

  32. Angellalow says:

    Just one dollar a month will not be reflected in your home budget, especially since you pay much more for the Internet and do not think about the costs, and your income will increase from month to month. You just need to program yourself to work for a long time in the digital business and remember that there are no freebies on the Internet. Money must be earned competently and honestly. To do this, there is a computer, the Internet and competent programmers – employers. On the Internet, 99.9% of all proposed projects for earnings are scams and hypes that are created by crooks and scammers. We are the 0.1% that people trust. We are not ashamed of our system! On the Internet, you will not find reviews from people who have been affected in our system. There are only a few paid fake experts who are trying to denigrate our company (although the <a and <a projects received <a from Yandex "a Site with a high degree of user engagement and loyalty")… These false experts advertise Scam banners on their websites, under the guise of alleged decency. This must be understood!

    Do you want to become a financially independent person? Go to our system where you will be helped, prompted and taught. We are in touch around the clock. <a

    <a
    <a
    <a
    <a
    <a

  33. web hosting services says:

    I think this is one of the most vital info for me. And i am happy reading your article. However want to statement on few normal things, The website taste is great, the articles is in reality great : D. Good process, cheers

  34. ezyspot.com ads says:

    I have been absent for a while, but now I remember why I used to love this website. Thank you, I’ll try and check back more frequently. How frequently you update your site?

  35. Jamesessen says:

    Знаете ли вы?
    Картина парада Победы, где руководство страны смещено на задний план, получила Сталинскую премию.
    Кустурица пропустил получение «Золотой ветви» в Каннах, так как любит друзей больше, чем церемонии награждения.
    Канадский солдат в одиночку освободил от немцев нидерландский город.
    Новый вид пауков-скакунов был назван по имени писателя в честь юбилея его самой известной книги о гусенице.
    Молнию можно не только увидеть, но и съесть.

    http://0pb8hx.com/

  36. Joker388.Net says:

    I really like your writing style, fantastic information, thanks for putting up :D. “You can complain because roses have thorns, or you can rejoice because thorns have roses.” by Ziggy.

  37. Daftar VivoSlot says:

    Absolutely written subject material, appreciate it for entropy. “The last time I saw him he was walking down Lover’s Lane holding his own hand.” by Fred Allen.

  38. Slot Joker123 says:

    Hello, Neat post. There is a problem with your web site in web explorer, would check thisK IE nonetheless is the marketplace leader and a big part of people will omit your magnificent writing due to this problem.

  39. VivoSlot Gamming says:

    Generally I don’t read article on blogs, but I would like to say that this write-up very forced me to check out and do so! Your writing style has been amazed me. Thanks, quite nice article.

  40. Joker123 Net says:

    I am really loving the theme/design of your blog. Do you ever run into any internet browser compatibility problems? A couple of my blog audience have complained about my site not operating correctly in Explorer but looks great in Chrome. Do you have any tips to help fix this problem?

  41. Slot Joker says:

    Please let me know if you’re looking for a article writer for your weblog. You have some really good articles and I feel I would be a good asset. If you ever want to take some of the load off, I’d love to write some articles for your blog in exchange for a link back to mine. Please shoot me an email if interested. Kudos!

  42. FafaSlot Online says:

    After study just a few of the blog posts in your website now, and I truly like your method of blogging. I bookmarked it to my bookmark web site list and can be checking again soon. Pls try my site as nicely and let me know what you think.

  43. Joker388 says:

    Howdy! I simply wish to give an enormous thumbs up for the nice information you might have right here on this post. I will probably be coming again to your weblog for more soon.

  44. PeterMug says:

    Узелки В – Среди заболеваний, которые передаются половым путем, самые распространенные такие: гонорея; педикулез, или лобковые вши; трихомоноз; цитомегаловирус; генитальный герпес; вирус папиломы человека (ВПЧ); кандидоз; хламидиоз; сифилис; контагиозный Вирус контагиозного моллюска Вирус контагиозного моллюска

  45. nunca fale que sua mulher esta gorda says:

    Hey there just wanted to give you a quick heads up and let you know a few of the images aren’t loading correctly. I’m not sure why but I think its a linking issue. I’ve tried it in two different internet browsers and both show the same results.

  46. nao chame sua mulher de obesa says:

    What i do not understood is actually how you’re no longer actually much more smartly-appreciated than you might be right now. You are so intelligent. You realize therefore significantly in relation to this topic, produced me in my view consider it from so many various angles. Its like women and men don’t seem to be interested unless it is something to do with Lady gaga! Your personal stuffs excellent. At all times handle it up!

  47. labedroomcoeft says:

    Bedroom la
    Come in outstanding store in Jefferson Park production for office and home use and cafe! presents over 12000 appointment furniture and goods for a country house and apartments or restaurant. Natural rattan , are used for the purposes of production our branded products , possesses strength and wear resistance, wonderful external data. All furniture processed special compounds, due to which their surface does not absorb water, stable to extremes ambient temperature air and exposure of the sun. Vya our furniture excellent retains its functions even in restaurant in open spaces . In the presented online catalog you offered photos furniture for dining room, hall, bedroom, children’s room , as well as intended for organization of storage area – dressers and cabinets, cabinets and others . In our store in Fairfax you can buy everything for any your home not expensive . We invite client come to store , holding in Westwood what cares about its shopper.

  48. Mitologia says:

    Thanks for your own hard work on this web site. My mother enjoys making time for research and it is simple to grasp why. A lot of people notice all regarding the lively means you produce precious tips and tricks via the web blog and even boost response from visitors on the issue plus our princess is in fact learning a whole lot. Have fun with the remaining portion of the year. You are carrying out a fantastic job.

  49. kevin david entrepreneur says:

    Hi there, just changed into aware of your weblog via Google, and found that it’s really informative. I am going to be careful for brussels. I will appreciate in case you proceed this in future. Numerous folks shall be benefited from your writing. Cheers!

  50. Neida Calderara says:

    Hello! Do you know if they make any plugins to assist with SEO? I’m trying to get my blog to rank for some targeted keywords but I’m not seeing very good gains. If you know of any please share. Appreciate it!

  51. Joker123 Net says:

    Do you mind if I quote a few of your posts as long as I provide credit and sources back to your blog? My blog site is in the exact same area of interest as yours and my users would really benefit from some of the information you present here. Please let me know if this ok with you. Regards!

  52. VivoSlot Gamming says:

    I discovered your blog site on google and check a few of your early posts. Continue to keep up the very good operate. I just additional up your RSS feed to my MSN News Reader. Seeking forward to reading more from you later on!…

  53. Slot Joker388 says:

    What i don’t realize is in reality how you’re not really a lot more neatly-favored than you might be right now. You’re very intelligent. You know thus significantly when it comes to this subject, made me for my part imagine it from a lot of various angles. Its like women and men aren’t interested unless it¦s something to do with Girl gaga! Your individual stuffs great. Always take care of it up!

  54. FafaSlot says:

    I’m still learning from you, as I’m trying to achieve my goals. I definitely enjoy reading all that is posted on your blog.Keep the posts coming. I enjoyed it!

  55. VivoSlot Online says:

    Excellent read, I just passed this onto a friend who was doing a little research on that. And he actually bought me lunch as I found it for him smile Therefore let me rephrase that: Thanks for lunch! “The guy with the biggest stomach will be the first to take off his shirt at a baseball game.” by Glenn Dickey.

  56. furnitopiacer says:

    Top 3 luxury furniture – sofa
    Our online store international company provides discount, write in an online chat.At the present time you are in best Our оnline store specialized firms in Bel air products for garden and home use and office modern lighting stores los angeles. Firm sells over 5000 products for a country house and houses or cafe-bar and piece of furniture.Natural tree, the that are used for the purpose products, has at its disposal reliability and wear resistance, delightful external data. All pieces of furniture processed particular compounds, because of which their surface does not absorb water, resistant to extremes temperature and influence of the sun. Wicker furniture excellent retains its functions even in cafe in open spaces. We catalog for you offered photographs furniture for dining room, hall, bedroom, children’s room, as well as intended for storage of things – cabinets, chests of drawers and many others. We are waiting client visit our large online store, company in Miracle milethat appreciates all of its shopper. On portal our store you waiting colossal choice at cost. Our catalog contains price lists, Label data about types details potential kinds complete modules.Each item furniture produced directly from factory manufacturer. Decrease prices on Label achieved based on the absence of trade floor space, for rent which necessary pay and smallest staff workers. Minimum costs enable to establish affordable prices for every buyer for all groups goods. Want update your interior? Read carefully the news products home from modern collection, in her represented as products with colorful floral patterns and colors and products with chic texture finish metallic. Furniture Items for the garden is rightfully considered necessary attribute each modern housing. In the presented store online you can buy reliable furniture for garden and home in SANTA MONICAto you all furniture at desired time day.Prices, that provides online shop furniture items BEL AIR AND HOLMBY HILLS very visitor.As a rule small fee may cause certain questions regarding good quality products affordable modern contemporary furniture. The catalog which filled diverse items furniture composes only first-class factory products.

  57. job advertising site says:

    You could certainly see your enthusiasm within the work you write. The arena hopes for more passionate writers such as you who aren’t afraid to say how they believe. Always go after your heart.

  58. สล็อต says:

    Great blog here! Also your website loads up very fast! What host are you using? Can I get your affiliate link to your host? I wish my web site loaded up as fast as yours lol

  59. Rickeyvon says:

    Наша компания занимается расскруткой продвижение сайта буржунет совершенно не дорого. В случае, если у вас существует свой бизнес, тогда рано или поздно вы лично осознаете, что без оптимизация и продвижение сайтов сшау вас нет возможности работать дальше. Сейчас фирма, которая подумывают о собственном будущем развитии, должна иметь веб-сайт для seo продвижение сайтов google. продвижение англоязычного сайта в google- способ, используя который возможно приобретать новых покупателей, и дополнительно получить проценты, с тем чтобы рассказать об наличии вашей собственной производственной компании, её продуктах, функциях. Специализированная международная фирма сделать для вашей фирмы инструмент, с помощью которого вы сможете залучать правильных партнеров, получать прибыль и расти.Продающийся сайт- лицо фирмы, в связи с этим имеет значение, кому вы доверяете создание своего веб страницы. Мы – команда профи, которые имеют обширный практический опыт конструирования электронную коммерцию с нуля, направления, разработанного типа. Сотрудники нашей фирмы неизменно действуем по результатом. Международная компания сумеет предоставить всем нашим заказчикам профессиональное сопровождение по доступной антикризисной расценке.Вы можете сделать заказ онлайн-визитку, рекламный сайт. Не сомневайтесь, что ваш портал будет разработан высококлассно, с разными самыми новыми технологиями.

    seo продвижение сайтов в сша

  60. Industry News says:

    I’m usually to blogging and i actually admire your content. The article has really peaks my interest. I’m going to bookmark your web site and preserve checking for new information.

  61. Game podcasts says:

    Excellent weblog right here! Also your site a lot up fast! What web host are you using? Can I am getting your affiliate hyperlink to your host? I wish my site loaded up as quickly as yours lol

  62. becoming a criminal lawyer says:

    Thanks for every other informative blog. Where else may I get that type of info written in such a perfect way? I’ve a project that I’m simply now operating on, and I have been at the glance out for such info.

  63. a good criminal lawyer says:

    Thanks, I have recently been searching for info approximately this topic for a long time and yours is the best I’ve found out till now. But, what in regards to the bottom line? Are you certain concerning the source?

  64. Stephenmomma says:

    Urgent loans in the USA. 5 minutes approval in https://maybeloan.com. Fast, easy, safe and secure. Online. – Need $1,000-$5,000 and more fast? – Yes. Bad credit OK. Instant approval. Online 24/7 Customer Service. Get Started now!

  65. Kevinloatt says:

    Наш знаменитый холдинг безграничным навыком в сфере производства телеинспекция трубопроводов и дополнительно производственных услуг, компания является производителем высококачественных работ для большинства ведущих фирм в стране. Наши сегодняшние квалифицированные услуги по телеинспекция каналов легкодоступны в Клинцы, затем чтобы оказать экономически действенность решения с целью всех разновидностей объектов. Наш специализированный холдинг для вас изготовление, современное строительство, ввода в эксплуатацию и поддержки. Консультационные услуги по моментам монтирования и цены их задач. Мы работаем как в угоду невеликих, так и в угоду крупных объектов, реализуя методы для произвольного фирмы где необходимы оборудование для телеинспекции скважин. Наше специализированное предприятие всегда готова предоставить знающий обслуживающий персонал в целях монтирования телеинспекция трубопроводов цена на предприятие заказчика. Как только лично вы выбираете нашу компании в качестве исполнителя, вы получите организацию с совершенным ассортиментом сервисных услуг.

    машина гидродинамической очистки

  66. Jerryjerty says:

    Знаете ли вы?
    Российская учёная показала, что проект «Новой Москвы» 1923 года воспроизводил план трёхвековой давности.
    Мама и четверо детей снимают фильмы о своей жизни во время войны.
    Герои украинского сериала о школьниках с трудом изъясняются по-украински.
    Американская энциклопедия включила в себя десятки статей о вымышленных людях, якобы связанных с Латинской Америкой.
    Среди клиентов древнеримского афериста был император Марк Аврелий.

    http://www.arbeca.net/

  67. hand sanitizer dispenser says:

    May I simply say what a relief to find a person that actually knows what they’re talking about on the web. You definitely realize how to bring a problem to light and make it important. More people really need to look at this and understand this side of your story. It’s surprising you’re not more popular since you surely have the gift.

  68. Britta Cottew says:

    Good write-up, I am regular visitor of one¦s website, maintain up the excellent operate, and It’s going to be a regular visitor for a lengthy time.

Trackbacks & Pingbacks

  1. walmart tylenol 500 mg

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

  2. buy naltrexone online usa

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

  3. viagra samples

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

  4. buy ciprofloxacin

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

  5. inhalers online without an rx

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

  6. doctor7online.com

    Magento 2 module development – A comprehensive guide – Part 2 – aionhills.com

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published.