Magento 2 module development – A comprehensive guide – Part 1

After a long anticipation, Magento 2.0 was launched at the end of 2015, whose development had started back in 2012. Its structure is significantly different from versions 1.x, which means that the extensions developed for Magento 1.x are not compatible with the new system. We now present how to create a basic Magento 2.0 extension (module).



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.

It is recommended to set the IDE properties to be used for Magento 2 developments. Click To Tweet

 

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

 

 


  • Jhe

    Can you help me to create a custom form in frontend like: Name, Age, Gender, Address and when the users click the submit button, all data will be save in your custom table

  • Simone

    Hi,

    great tutorial. How can I add different values according to store view?

  • Matias Castro

    Hi, thanks for the tutorial.

    I’m getting a blank page when I try to visit {localUrl}/test . Also, the system.log gives me this error:

    [2016-12-10 07:42:54] main.INFO: Cache file with merged layout: LAYOUT_frontend_STORE1_230c7f89f448f2b246fbb6eb69a626861 and handles default, test_index_index: Please correct the XML data and try again. [] []

    Any ideas? Thanks a lot.

    • Matias Castro

      My bad. Had the hole view subfolder missing.

      Great tutorial, thanks a lot!

  • smaugstheswagger

    I read a similar article on Magenticians blog, but it was a Magento 1 tutorial. The process was quite similar, may be there are only few codes that needs to be changed when it comes to Magento 2.

  • Mykhailo Shatilov

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

    you can use insertMultiple function.

  • Nishant Mathur

    can you help me to create a plugin uninstall script

  • alejduin

    sorry for my ignorance, these procedures work for the current version of Magento 2

  • alejduin

    sorry for my ignorance, these procedures work for the current version of Magento 2

  • Alejandro Duin

    These procedures work for Magento 2.1.8 version???

  • Prakash A

    Can any one help me in this query

    I have a table name testimonial_product contain product_id.

    and i have added fk as

    ->addForeignKey(
    $setup->getFkName(
    ‘testimonial_product’,
    ‘product_id’,
    ‘catalog_product_entity’,
    ‘entity_id’
    ),
    ‘product_id’,
    $installer->getTable(‘catalog_product_entity’),
    ‘entity_id’,
    MagentoFrameworkDBDdlTable::ACTION_CASCADE,
    MagentoFrameworkDBDdlTable::ACTION_CASCADE
    )
    it generate query as

    CREATE TABLE IF NOT EXISTS `testimonial_product` (
    `rel_id` int UNSIGNED NOT NULL auto_increment COMMENT ‘Relation ID’ ,
    `testimonial_id` int UNSIGNED NOT NULL default 0 COMMENT ‘Testimonial ID’ ,
    `product_id` int UNSIGNED NOT NULL default 0 COMMENT ‘product_id’ ,
    `position` int NOT NULL default 0 COMMENT ‘Position’ ,
    PRIMARY KEY (`rel_id`),
    INDEX `TESTIMONIAL_PRODUCT_PRODUCT_ID` (`product_id`),
    UNIQUE `TESTIMONIAL_PRODUCT_TESTIMONIAL_ID_PRODUCT_ID` (`testimonial_id`, `product_id`),
    CONSTRAINT `TESTIMONIAL_STORE_TESTIMONIAL_ID_TESTIMONIAL_ENTITY_ID` FOREIGN KEY (`testimonial_id`) REFERENCES `testimonial` (`entity_id`) ON DELETE CASCADE,
    CONSTRAINT `TESTIMONIAL_PRODUCT_PRODUCT_ID_CATALOG_PRODUCT_ENTITY_ENTITY_ID` FOREIGN KEY (`product_id`) REFERENCES `catalog_product_entity` (`entity_id`) ON DELETE CASCADE

    but it throws error like SQLSTATE[HY000]: General error: 1005 Can’t create table `asnv2`.`athlete_product` (errno: 150 “Foreign key constraint is incorrectly formed”)

  • rohimic

    One can upgrade magento 2 using the two basic methods, one is from admin panel and other is using composer.

Do you need our support?
  • Magento Site Check
  • Magento Code Audit
  • Magento SEO Audit
  • Magento Project Rescue
Request help

NEED A RELIABLE, PROFESSIONAL MAGENTO DEVELOPMENT PARTNER?

Contact us if you have any question or requirement related to the preparation of a new or renewal of an existing online store.

Next