Magento 2: Creating A full Magento 2 Module

A full Magento 2 Module for both frontend and backend

Posted on May 30, 2017 in Magento2

Create a full-fledged Module Step by Step

You could just follow my code to create this module from the scratch. Or you can directly download the compressed tar file and install it and play it. The Link to downlad is at the end of this tutorial.

Part 1: New Module for Front End

A full-fledged Module Creation for Magento 2.

Create a module files: module.xml


#app/code/Jeff/SimpleNews/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Jeff_SimpleNews" setup_version="0.0.2"/>
</config>

Create a module registration file


#app/code/Jeff/SimpleNews/registration.php

<?php
    \Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        'Jeff_SimpleNews',
        __DIR__
    );

The schema Installation file


#app/code/Jeff/SimpleNews/Setup/InstallSchema.php

<?php
namespace Jeff\SimpleNews\Setup;

use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\ModuleContextInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\DB\Ddl\Table;

class InstallSchema implements InstallSchemaInterface {
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context) {
        $installer = $setup;

        $installer->startSetup();

        $tableName = $installer->getTable('tutorial_simplenews');

        if($installer->getConnection()->isTableExists($tableName) != true) {
            $table = $installer->getConnection()
                ->newTable($tableName)
                ->addColumn(
                    'id',
                    Table::TYPE_INTEGER,
                    null,
                    [
                        'identity' => true,
                        'unsigned' => true,
                        'nullable' => false,
                        'primary' => true
                    ],
                    'ID'
                )
                ->addColumn(
                    'title',
                    Table::TYPE_TEXT,
                    null,
                    ['nullable' => false, 'default' => ''],
                    'Title'
                )
                ->addColumn(
                    'summary',
                    Table::TYPE_TEXT,
                    null,
                    ['nullable' => false, 'default' => ''],
                    'Summary'
                )
                ->addColumn(
                    'description',
                    Table::TYPE_TEXT,
                    null,
                    ['nullable' => false, 'default' => ''],
                    'Description'
                )
                ->addColumn(
                    'created_at',
                    Table::TYPE_DATETIME,
                    null,
                    ['nullable' => false],
                    'Created At'
                )
                ->addColumn(
                    'status',
                    Table::TYPE_SMALLINT,
                    null,
                    ['nullable' => false, 'default' => '0'],
                    'Status'
                )
                ->setComment('News Table')
                ->setOption('type', 'InnoDB')
                ->setOption('charset', 'utf8');
            $installer->getConnection()->createTable($table);
        }

        $installer->endSetup();
    }
}

Data Upgrade script to insert some data


#app/code/Jeff/SimpleNews/Setup/UpgradeData.php

<?php
namespace Jeff\SimpleNews\Setup;

use Magento\Framework\Setup\UpgradeDataInterface;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;

class UpgradeData implements UpgradeDataINterface {
    public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) {
        $setup->startSetup();

        if(version_compare($context->getVersion(), '0.0.2') < 0) {
            $tableName = $setup->getTable('tutorial_simplenews');

            if($setup->getConnection()->isTableExists($tableName) == true ) {
                $data = [
                    [
                        'title'=>'How to create a simple module',
                        'summary' => 'The Summary',
                        'description' => 'The Description',
                        'created_at' => date('Y-m-d H:i:s'),
                        'status' => 1
                    ],
                    [
                        'title'=>'How to use model and collection in magento 2',
                        'summary' => 'The Summary',
                        'description' => 'The Description',
                        'created_at' => date('Y-m-d H:i:s'),
                        'status' => 1
                    ],
                    [
                        'title'=>'Create a module with custom database table',
                        'summary' => 'The Summary',
                        'description' => 'The Description',
                        'created_at' => date('Y-m-d H:i:s'),
                        'status' => 1
                    ],
                ];

                foreach($data as $item) {
                    $setup->getConnection()->insert($tableName, $item);
                }
            }
        }

        $setup->endSetup();
    }
}

Create Model News for business Logic


#app/Jeff/SimpleNews/Model/News.php

<?php
namespace Jeff\SimpleNews\Model;

use Magento\Framework\Model\AbstractModel;

class News extends AbstractModel {
    protected function _construct() {
        /** @var resourceModel classname */
        $this->_init('Jeff\SimpleNews\Model\ResourceModel\News');
    }
}

Create Model's ResourceModel to handle real database transaction


#app/code/Jeff/SimpleNews/Model/ResourceModel/News.php
<?php
namespace Jeff\SimpleNews\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class News extends AbstractDb {
    protected function _construct() {
        $this->_init('tutorial_simplenews', 'id');
    }
}

Create Model's collection class


#app/code/Jeff/SimpleNews/ModelResourceModel/News/Collection.php
<?php
namespace Jeff\SimpleNews\Model\ResourceModel\News;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;

class Collection extends AbstractCollection {
    protected function _construct() {
        $this->_init('Jeff\SimpleNews\Model\News', 'Jeff\SimpleNews\Model\ResourceModel\News');
    }
}

Setup the frontend route


#app/code/Jeff/SimpleNews/etc/frontend/routes.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="standard">
        <route id="news" frontName="news">
            <module name="Jeff_SimpleNews"/>
        </route>
    </router>
</config>

Create IndexController


#app/code/Jeff/SimpleNews/Controller/Index/Index.php
<?php
namespace Jeff\SimpleNews\Controller\Index;

use Jeff\SimpleNews\Model\NewsFactory;

class Index extends \Magento\Framework\App\Action\Action
{

    protected $resultPageFactory;

    protected $_modelNewsFactory;

    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $resultPageFactory,
        NewsFactory $modelNewsFactory
        )
    {
        $this->resultPageFactory = $resultPageFactory;
        $this->_modelNewsFactory = $modelNewsFactory;
        parent::__construct($context);
    }

    public function execute()
    {
        //return $this->resultPageFactory->create();  
        $newsModel = $this->_modelNewsFactory->create();

        $item = $newsModel->load(1);
        var_dump($item->getData());

        $newsCollection = $newsModel->getCollection();
        var_dump($newsCollection->getData());
    }
}

Setup Module's backend configuration


#app/code/Jeff/SimpleNews/etc/adminhtml/system.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../Backend/etc/system_file.xsd">
    <system>
        <tab id="tutorial" translate="label" sortOrder="1">
            <label>Jeff Tutorial</label>
        </tab>
        <section id="simplenews" translate="label" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
            <label>Simple News</label>
            <tab>tutorial</tab>
            <resource>Jeff_SimpleNews::system_config</resource>
            <group id="general" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
                <label>General Settings</label>
                <field id="enable_in_frontend" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Enable In Frontend</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="head_title" translate="label comment" type="text" sortOrder="2" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Head Title</label>
                    <comment>Fill head title of news list page at here</comment>
                    <validate>required-entry</validate>
                </field>
                <field id="latest_news_block_position" translate="label" type="select" sortOrder="3" showInDefault="1" showInWebsite="1" showInStore="1">
                    <label>Latest News Block Position</label>
                    <source_model>Jeff\SimpleNews\Model\System\Config\LatestNews\Position</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

Create a custom source model


#app/code/Jeff/SimpleNews/Model/System/Config/LatestNews/Position.php

<?php
namespace Jeff\SimpleNews\Model\System\Config\LatestNews;

use Magento\Framework\Option\ArrayInterface;

class Position implements ArrayInterface {
    const LEFT = 1;
    const RIGHT = 2;
    const DISABLED = 0;

    public function toOptionArray() {
        return [
            self::LEFT => __("Left"),
            self::RIGHT => __('Right'),
            self::DISABLED => __('Disabled')
        ];
    }
}

Create a role for this config section


#app/code/Jeff/SimpleNews/etc/acl.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/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="Jeff_SimpleNews::system_config" title="Simple News Section" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

Set some default value for configuration options


#app/code/Jeff/SimpleNews/etc/config.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../Core/etc/config.xsd">
    <default>
        <simplenews>
            <general>
                <enable_in_frontend>1</enable_in_frontend>
                <head_title>Tutorial - Simple News</head_title>
                <latest_news_position>1</latest_news_position>
            </general>
        </simplenews>
    </default>
</config>

Create a Helper Data class


#app/code/Jeff/SimpleNews/Helper/Data.php
<?php 
namespace Jeff\SimpleNews\Helper;

use Magento\Framework\App\Helper\AbstractHelper;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Helper\Context;
use Magento\Store\Model\ScopeInterface;

class Data extends AbstractHelper {
    const XML_PATH_ENABLED = 'simplenews/general/enable_in_frontend';
    const XML_PATH_HEAD_TITLE = 'simplenews/general/head_title';
    const XML_PATH_LATEST_NEWS = 'simplenews/general/latest_news_block_position';

    protected $_scopeConfig;

    /**
     * @param Context $contex
     * @param ScopeConfigInterface $scopeConfig
     */
    public function __construct(Context $context, ScopeConfigInterface $scopeConfig) {
        $this->_scopeConfig = $scopeConfig;
        parent::__construct($context);
    }

    public function isEnabledInFrontend($store = null) {
        return $this->_scopeConfig->getValue(self::XML_PATH_ENABLED, ScopeInterface::SCOPE_STORE);
    }

    public function getHeadTitle() {
        return $this->_scopeConfig->getValue(self::XML_PATH_HEAD_TITLE, ScopeInterface::SCOPE_STORE);
    }

    public function getLatestNewsBlockPosition() {
        return $this->_scopeConfig->getValue(self::XML_PATH_LATEST_NEWS, ScopeInterface::SCOPE_STORE);
    }
}

Create Layout file for page handle


#app/code/Jeff/SimpleNews/view/frontend/layout/news_news.xml

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="3columns"  xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
    <head>
        <css src="Jeff_SimpleNews::css/style.css" />
    </head>
    <body>
        <referenceContainer name="sidebar.main">
            <block class="Jeff\SimpleNews\Block\Latest\Left" name="latest.news.left" before="-" />
        </referenceContainer>
        <referenceContainer name="sidebar.additional">
            <block class="Jeff\SimpleNews\Block\Latest\Right" name="latest.news.right" before="-" />
        </referenceContainer>
    </body>
</page>

Create another layout file by update the previous layout


#app/code/Jeff/SimpleNews/view/frontend/layout/news_index_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="3columns" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="news_news" />
    <body>
        <referenceBlock name="content">
            <block template="Jeff_SimpleNews::list.phtml" class="Jeff\SimpleNews\Block\NewsList" name="jeff_simplenews_block_news_list"/>
        </referenceBlock>
    </body>
</page>

Create Block NewList file


#app/code/Jeff/SimpleNews/Block/NewsList.php

<?php
namespace Jeff\SimpleNews\Block;

use Magento\Framework\View\Element\Template;
use Jeff\SimpleNews\Helper\Data;
use Jeff\SimpleNews\Model\NewsFactory;

class NewsList extends \Magento\Framework\View\Element\Template
{
    protected $_newsFactory;

    public function __construct(
        \Magento\Framework\View\Element\Template\Context $context, 
        NewsFactory $newsFactory, 
        array $data = []
    ) {
        $this->_newsFactory = $newsFactory;
        parent::__construct($context, $data);
    }

    protected function _construct() {
        parent::_construct();
        $collection = $this->_newsFactory->create()->getCollection()->setOrder('id', 'DESC');
        $this->setCollection($collection);
    }

    protected function _prepareLayout(){
        parent::_prepareLayout();

        $pager = $this->getLayout()->createBlock('Magento\Theme\Block\Html\Pager', 'simplenews.news.list.pager');

        $pager->setLimit(5)->setShowAmount(false)->setCollection($this->getCollection());
        $this->setChild('pager', $pager);
        $this->getCollection()->load();

        return $this;
    }

    public function getPagerHtml() {
        return $this->getChildHtml('pager');
    }
}

Create frontend template file list.phtml


#app/code/Jeff/SimpleNews/view/frontend/templates/list.phtml

<div class="simplenews">
    <?php
        $newsCollection = $block->getCollection();
        if($newsCollection->getSize() > 0) :
    ?>
    <div class="toolbar top">
        <?php echo $block->getPagerHtml(); ?>
    </div>
    <ul>
        <?php foreach($newsCollection as $news): ?>
            <li>
                <div class="simplenews-list">
                    <a class="news-title" href="<?php echo $this->getUrl('news/index/view', ['id'=>$news->getId()]) ?>"><?php echo $news->getTitle()?></a>
                    <div class="simplenews-list-content">
                        <?php echo $news->getSummary() ?>
                    </div>
                </div>
            </li>
        <?php endforeach; ?>
    </ul>
    <div style="clear:both"></div>
    <div class="toolbar-bottom">
        <div class="toolbar bottom">
            <?php echo $block->getPagerHtml();?>
        </div>
    </div>
    <?php else: ?>
        <p><?php echo __('Have not article!')?></p>
    <?php endif;?>
</div>

Create an abstract class by extending Magento Core Action class


#app/code/Jeff/SimpleNews/Controller/News.php

<?php
namespace Jeff\SimpleNews\Controller;

use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\View\Result\PageFactory;
use Jeff\SimpleNews\Helper\Data;
use Jeff\SimpleNews\Model\NewsFactory;

abstract class News extends Action 
{
    protected $_pageFactory;
    protected $_dataHelper;
    protected $_newsFactory;

    /*
        The order is always based on the parameters of the original constructor, extra parameters go in the end
    */
    public function __construct(
        \Magento\Framework\App\Action\Context $context,
        \Magento\Framework\View\Result\PageFactory $pageFactory, 
        \Jeff\SimpleNews\Helper\Data $helper, 
        \Jeff\SimpleNews\Model\NewsFactory $newsFactory
    ){
        $this->_pageFactory = $pageFactory;
        $this->_dataHelper = $helper;
        $this->_newsFactory = $newsFactory;
        parent::__construct($context);
    }

    public function dispatch(RequestInterface $request) {
        if($this->_dataHelper->isEnabledInFrontend()) {
            $result = parent::dispatch($request);
            return $result;
        }
        else {
            $this->_forward('noroute');
        }
    }
}

Update Index Controller by extends the abstract class 'New.php'


#app/code/Jeff/SimpleNews/Controller/Index/Index.php

<?php
namespace Jeff\SimpleNews\Controller\Index;

use Jeff\SimpleNews\Controller\News;

class Index extends News
{

    public function execute()
    {
        $pageFactory = $this->_pageFactory->create();

        $pageFactory->getConfig()->getTitle()->set($this->_dataHelper->getHeadTitle());

        //Add breadcrumb
        $breadcrumbs = $pageFactory->getLayout()->getBlock('breadcrumbs');
        $breadcrumbs->addCrumb('home', ['label'=>__('Home'), 'title'=>__('Home'), 'link'=>$this->_url->getUrl('')]);
        $breadcrumbs->addCrumb('simplenews', ['label'=>__('Simple News'), 'title'=>__('Simple News')]);

        return $pageFactory;
    }
}

Create a layout file for news detail page


#app/code/Jeff/SimpleNews/view/frontend/layout/news_index_view.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="3columns" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
    <update handle="news_news" />
    <body>
        <referenceContainer name="content">
            <block class="Jeff\SimpleNews\Block\View" name="jeff_simplenews_news_view" template="Jeff_SimpleNews::view.phtml" />
        </referenceContainer>
    </body> 
</page>

Create News view action


#app/code/Jeff/SimpleNews/Controller/Index/View.php

<?php
namespace Jeff\SimpleNews\Controller\Index;

use Jeff\SimpleNews\Controller\News;

class View extends News {
    public function execute() {
        $newsId = $this->getRequest()->getParam('id');

        $news = $this->_newsFactory->create()->load($newsId);

        $this->_objectManager->get('Magento\Framework\Registry')->register('newsData', $news);

        $pageFactory = $this->_pageFactory->create();

        $pageFactory->getConfig()->getTitle()->set($news->getTitle());

        $breadcrumbs = $pageFactory->getLayout()->getBlock('breadcrumbs');
        $breadcrumbs->addCrumb('home', ['label' => __('Home'), 'title'=>__('Home'), 'link' => $this->_url->getUrl('')]);
        $breadcrumbs->addCrumb('simplenews', ['label' => __('Simple News'), 'title'=>__('Simple News'), 'link' => $this->_url->getUrl('news')]);
        $breadcrumbs->addCrumb('news', ['label' => $news->getTitle(), 'title'=>$news->getTitle()]);

        return $pageFactory;
    }
}

create view news block


#app/code/Jeff/SimpleNews/Block/View.php

<?php
namespace Jeff\SimpleNews\Block;

use Magento\Framework\View\Element\Template;
use Magento\Framework\Registry;

class View extends Template {
    protected $_coreRegistry;

    public function __construct( Template\Context $context, Registry $coreRegistry, array $data =[]) {
        $this->_coreRegistry = $coreRegistry;
        parent::__construct($context, $data);
    }

    public function getNewsInformation() {
        return $this->_coreRegistry->registry('newsData');
    }
}

Create news view template file


#app/code/Jeff/SimpleNews/view/frontend/templates/view.phtml

<?php
$news = $block->getNewsInformation();
?>
<div class="mw-simplenews">
    <?php echo $news->getDescription() ?>
</div>

Create CSS file for styling the frontend Page


#app/code/Jeff/SimpleNews/view/frontend/web/css/style.css

.simplenews > ul {
   list-style: none;
   padding: 0;
}
.simplenews > ul li {
   padding: 10px 5px;
   margin: 0;
   background-color: #fff;
   border-bottom: 1px #c4c1bc solid;
   display: inline-block;
   width: 100%;
}
.simplenews > ul li:last-child {
   border-bottom: none;
}
.simplenews-list {
   float: left;
   position: relative;
   margin-left: 10px;
   width: 100%;
}
.simplenews-list a.news-title {
   font-weight: bold;
}
.simplenews-list a.news-title:hover {
   text-decoration: none;
}
.block-simplenews .block-title {
   margin: 0px 0px 20px;
}
.block-simplenews-heading {
   font-size: 18px;
   font-weight: 300;
}

Create Latest New Block


#app/code/Jeff/SimpleNews/Block/Latest.php

<?php
namespace Jeff\SimpleNews\Block;

use Magento\Framework\View\Element\Template;
use Jeff\SimpleNews\Helper\Data;
use Jeff\SimpleNews\Model\NewsFactory;
use Jeff\SimpleNews\Model\System\Config\Status;

class Latest extends Template {
    protected $_dataHelper;
    protected $_newsFactory;

    public function __construct(Template\Context $context, Data $dataHelper, NewsFactory $newsFactory) {
        $this->_dataHelper = $dataHelper;
        $this->_newsFactory = $newsFactory;
        parent::__construct($context);
    }

    //Get five latest news
    public function getLatestNews() {
        $collection = $this->_newsFactory->create()->getCollection();
        $collection->addFieldToFilter('status', ['eq'=> 1]);
        $collection->getSelect()->order('id DESC')->limit(5);

        return $collection;
    }
}

Create a Block for positioning the latest news: Left or Right

Left:

#app/code/Jeff/SimpleNews/Block/Latest/Left.php
<?php
namespace Jeff\SimpleNews\Block\Latest;

use Jeff\SimpleNews\Block\Latest;
use Jeff\SimpleNews\Model\System\Config\LatestNews\Position;

class Left extends Latest {
    public function _construct() {
         $position = $this->_dataHelper->getLatestNewsBlockPosition();

         if($position == Position::LEFT) {
            $this->setTemplate('Jeff_SimpleNews::latest.phtml');
         }
    }
}

Right:

#app/code/Jeff/SimpleNews/Block/Latest/Left.php

<?php
namespace Jeff\SimpleNews\Block\Latest;

use Jeff\SimpleNews\Block\Latest;
use Jeff\SimpleNews\Model\System\Config\LatestNews\Position;

class Right extends Latest {
    public function _construct() {
         $position = $this->_dataHelper->getLatestNewsBlockPosition();

         if($position == Position::RIGHT) {
            $this->setTemplate('Jeff_SimpleNews::latest.phtml');
         }
    }
}


Create the template file for Latest News


#app/code/Jeff/SimpleNews/view/frontend/template/latest.phtml

<?php
    $latestNews = $block->getLatestnews();
    if($latestNews->getSize() > 0) :
?>
<div class="block block-simplenews">
    <div class="block-title">
        <strong class="block-simplenews-heading"><?php echo __('Latest News') ?></strong>
    </div>

     <div class="block-content">
         <?php foreach ($latestNews as $news) : ?>
            <div>
               <span>+ </span>
               <a href="<?php echo $this->getUrl('news/index/view', ['id' => $news->getId()])?>">
                  <span><?php echo $news->getTitle() ?></span>
               </a>
            </div>
         <?php endforeach; ?>
      </div>
</div>
<?php endif; ?>

Frontend view for the module

magento 2 backend 2

Part 2: New Module for Back End

Create the menu for Magento backend


#app/code/Jeff/SimpleNews/etc/adminhtml/menu.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../Backend/etc/menu.xsd">
    <menu>
        <!-- The resource is the acl name -->
        <add id="Jeff_SimpleNews::main_menu" 
            title="Simple News" 
            module="Jeff_SimpleNews" 
            sortOrder="20" 
            resource="Jeff_SimpleNews::simplenews" />

        <add id="Jeff_SimpleNews::add_news" 
            title="Add News" 
            module="Jeff_SimpleNews" 
            sortOrder="1" 
            parent="Jeff_SimpleNews::main_menu" 
            action="simplenews/news/new" 
            resource="Jeff_SimpleNews::manage_news" />

        <add id="Jeff_SimpleNews::manage_news" 
            title="Manage News" 
            module="Jeff_SimpleNews" 
            sortOrder="2" 
            parent="Jeff_SimpleNews::main_menu" 
            action="simplenews/news/index" 
            resource="Jeff_SimpleNews::manage_news" />

        <add id="Jeff_SimpleNews::configuration" 
            title="Configuration" 
            module="Jeff_SimpleNews" 
            sortOrder="3" 
            parent="Jeff_SimpleNews::main_menu" 
            action="adminhtml/system_config/edit/section/simplenews" 
            resource="Jeff_SimpleNews::configuration" />
    </menu>
</config>

Create backend route file


#app/code/Jeff/SimpleNews/etc/adminhtml/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="simplenews" frontName="simplenews">
            <module name="Jeff_SimpleNews"/>
        </route>
    </router>
</config>

Update the acl.xml to add more roles


#app/code/Jeff/SimpleNews/etc/acl.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Jeff_SimpleNews::simplenews" title="Simple News" sortOrder="100">
                    <resource id="Jeff_SimpleNews::add_news" title="Add News" sortOrder="1" />
                    <resource id="Jeff_SimpleNews::manage_news" title="Manage News" sortOrder="2" />
                    <resource id="Jeff_SimpleNews::configuration" title="Configuration" sortOrder="3" />
                </resource>

                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="Jeff_SimpleNews::system_config" title="Simple News Section" />
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

Create layout for grid



#app/code/Jeff/SimpleNews/view/adminhtml/layout/simplenews_news_grid_block.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="jeff_simplenews_news.grid.container">
            <block class="Magento\Backend\Block\Widget\Grid" name="jeff_simplenews_news.grid" as="grid">
                <arguments>
                    <argument name="id" xsi:type="string">newsGrid</argument>
                    <argument name="dataSource" xsi:type="object">Jeff\SimpleNews\Model\ResourceModel\News\Collection</argument>
                    <argument name="default_sort" xsi:type="string">id</argument>
                    <argument name="default_dir" xsi:type="string">desc</argument>
                    <argument name="save_parameters_in_session" xsi:type="boolean">true</argument>
                    <argument name="use_ajax" xsi:type="boolean">true</argument>
                    <argument name="grid_url" xsi:type="url" path="*/*/grid"><param name="_current">1</param></argument>
                </arguments>

                <block class="Magento\Backend\Block\Widget\Grid\Massaction" name="jeff_simplenews_news.grid.massaction" as="grid.massaction">
                    <arguments>
                        <argument name="massaction_id_field" xsi:type="string">id</argument>
                        <argument name="form_field_name" xsi:type="string">news</argument>
                        <argument name="options" xsi:type="array">
                            <item name="label" xsi:type="array">
                                <item name="label" xsi:type="string" translate="true">Delete</item>
                                <item name="url" xsi:type="string">*/*/massDelete</item>
                                <item name="confirm" xsi:type="string" translate="true">Are your sure you want to delete?</item>
                            </item>
                        </argument>
                    </arguments>
                </block>

                <block class="Magento\Backend\Block\Widget\Grid\ColumnSet" name="jeff_simplenews_news.grid.columnSet" as="grid.columnSet">
                    <arguments>
                        <argument name="rowUrl" xsi:type="array">
                            <item name="path" xsi:type="string">*/*/edit</item>
                            <item name="extraParamsTemplate" xsi:type="array">
                                <item name="id" xsi:type="string">getId</item>
                            </item>
                        </argument>
                    </arguments>
                        
                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="id">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">ID</argument>
                            <argument name="type" xsi:type="string">number</argument>
                            <argument name="id" xsi:type="string">id</argument>
                            <argument name="index" xsi:type="string">id</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="title">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Title</argument>
                            <argument name="index" xsi:type="string">title</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="summary">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Summary</argument>
                            <argument name="index" xsi:type="string">summary</argument>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="status">
                        <arguments>
                            <argument name="header" xsi:type="string" translate="true">Status</argument>
                            <argument name="index" xsi:type="string">status</argument>
                            <argument name="type" xsi:type="string">options</argument>
                            <argument name="options" xsi:type="options" model="Jeff\SimpleNews\Model\System\Config\Status"/>
                        </arguments>
                    </block>

                    <block class="Magento\Backend\Block\Widget\Grid\Column" as="action" acl="Jeff_SimpleNews::manage_news">
                        <arguments>
                            <argument name="id" xsi:type="string">action</argument>
                            <argument name="header" xsi:type="string" translate="true">Action</argument>
                            <argument name="type" xsi:type="string">action</argument>
                            <argument name="getter" xsi:type="string">getId</argument>
                            <argument name="filter" xsi:type="boolean">false</argument>
                            <argument name="sortable" xsi:type="boolean">false</argument>
                            <argument name="index" xsi:type="string">stores</argument>
                            <argument name="is_system" xsi:type="boolean">true</argument>
                            <argument name="actions" xsi:type="array">
                                <item name="view_action" xsi:type="array">
                                    <item name="caption" xsi:type="string" translate="true">Edit</item>
                                    <item name="url" xsi:type="array">
                                        <item name="base" xsi:type="string">*/*/edit</item>
                                    </item>
                                    <item name="field" xsi:type="string">id</item>
                                </item>
                            </argument>
                            <arugment name="header_css_class" xsi:type="string">col-actions</arugment>
                            <arugment name="column_css_class" xsi:type="string">col-actions</arugment>
                        </arguments>
                    </block>
                </block>
            </block>
        </referenceBlock>
    </body>
</page>

Create layout for Grid Container


#app/code/Jeff/SimpleNews/view/adminhtml/layout/simplenews_news_index.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="formkey" />
    <update handle="simplenews_news_grid_block" />

    <body>
        <referenceBlock name="content">
            <block class="Jeff\SimpleNews\Block\Adminhtml\News" name="jeff_simplenews_news.grid.container" />
        </referenceBlock>
    </body>
</page>
<!-- This file is used to declare grid container block -->

Create layout for ajax load


#app/code/Jeff/SimpleNews/view/adminhtml/layout/simplenews_news_grid.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/layout_generic.xsd">
    <update handle="formkey" />
    <update handle="simplenews_news_grid_block" />
    <container name="root">
        <block class="Magento\Backend\Block\Widget\Grid\Container" name="jeff_simplenews_news.grid.container" template="Magento_Backend::widget/grid/container/empty.phtml"/>
    </container>
</page>

Create news status option file


#app/code/Jeff/SimpleNews/Model/System/Config/Status.php
<?php
namespace Jeff\SimpleNews\Model\System\Config;

use Magento\Framework\Option\ArrayInterface;

class Status implements ArrayInterface {
    const ENABLED = 1;
    const DISABLED = 0;

    public function toOptionArray() {
        $options = [
            self::ENABLED => __('Enabled'),
            self::DISABLED => __('Disabled')
        ];

        return $options;
    }
}

Create News Block for backend


#app/code/Jeff/SimpleNews/Block/Adminhtml/News.php

<?php
namespace Jeff\SimpleNews\Block\Adminhtml;

use Magento\Backend\Block\Widget\Grid\Container;

class News extends Container {
    protected function _construct() {
        $this->_controller = 'adminhtml_news'; //controller name
        $this->_blockGroup = 'Jeff_SimpleNews'; //Module name
        $this->_headerText = __('Manage News');
        $this->_addButtonLabel = __('Add News');
        parent::_construct();
    }
}

Create Grid block file for Ajax load


#app/code/Jeff/SimpleNews/Block/Adminhtml/News/Grid.php
<?php
namespace Jeff\SimpleNews\Block\Adminhtml\News;

use Magento\Backend\Block\Widget\Grid as WidgetGrid;

class Grid extends WidgetGrid {

}

Create a backend controller file for child action class to extend


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News.php
<?php
namespace Jeff\SimpleNews\Controller\Adminhtml;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\Registry;
use Magento\Framework\View\Result\PageFactory;
use Jeff\SimpleNews\Model\NewsFactory;

abstract class News extends Action {
    protected $_coreRegistry;

    protected $_resultPageFactory;

    protected $_newsFactory;

    public function __construct(
        Context $context,
        Registry $coreRegistry,
        PageFactory $resultPageFactory,
        NewsFactory $newsFactory
    ) {
        $this->_coreRegistry = $coreRegistry;
        $this->_resultPageFactory = $resultPageFactory;
        $this->_newsFactory = $newsFactory;
        parent::__construct($context);
    }

    protected function _isAllowed() {
        return $this->_authorization->isAllowed('Jeff_SimpleNews::manage_news');
    }
}

Create Backend Action file Index.php


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/Index.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

class Index extends News {
    public function execute() {
        if($this->getRequest()->getQuery('ajax')) {
            $this->_forward('grid');
            return;
        }

        $resultPage = $this->_resultPageFactory->create();
        $resultPage->setActiveMenu('Jeff_SimpleNews::main_menu');
        $resultPage->getConfig()->getTitle()->prepend(__('Jeff Simple News'));

        return $resultPage;
    }
}

Create another Action for ajax


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/Grid.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

/*
 This is the grid action which is used for loading grid by ajax
*/
class Grid extends News {
    public function execute() {
        return $this->_resultPageFactory->create();
    }
}

Create layout file simplenews_news_edit.xml for edit form


#app/code/Jeff/SimpleNews/view/adminhtml/layout/simplenews_news_edit.xml
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
layout="admin-2columns-left" xsi:noNamespaceSchemaLocation="../../../../../../../lib/internal/Magento/Framework/View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="left">
            <block class="Jeff\SimpleNews\Block\Adminhtml\News\Edit\Tabs" name="jeff_simplenews_news.edit.tabs" />
        </referenceContainer>
        <referenceContainer name="content">
            <block class="Jeff\SimpleNews\Block\Adminhtml\News\Edit" name="jeff_simplenews_news.edit" />
        </referenceContainer>
    </body>
</page>

Create the layout for create form


#app/code/Jeff/SimpleNews/view/adminhtml/layout/simplenews_news_create.xml

<?xml version="1.0"?>
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../Magento/Core/etc/layout_single.xsd">
    <update handle="simplenews_news_edit" />
</layout>

Create a form container block


#app/code/Jeff/Simplenews/Block/Adminhtml/News/Edit.php

<?php
namespace Jeff\SimpleNews\Block\Adminhtml\News;

use Magento\Backend\Block\Widget\Form\Container;
use Magento\Backend\Block\Widget\Context;
use Magento\Framework\Registry;

/* This is the block file of form container */
class Edit extends Container {
    protected $_coreRegistry = null;

    public function __construct(Context $context, Registry $registry, array $data = []) {
        $this->_coreRegistry = $registry;
        parent::__construct($context, $data);
    }

    protected function _construct() {
        $this->_objectId = 'id';
        $this->_controller = 'adminhtml_news';
        $this->_blockGroup = 'Jeff_SimpleNews';

        parent::_construct();

        $this->buttonList->update('delete', 'label', __('Delete'));
    }

    public function getHeaderText() {
        $newsRegistry = $this->_coreRegistry->registry('simplenews_news');

        if($newsRegistry->getId()) {
            $newsTitle = $this->escapeHtml($newsRegistry->getTitle());
            return __("Edit News '%1'", $newsTitle);
        }
        else {
            return __('Add News');
        }
    }

    protected function _prepareLayout() {
        $this->_formScripts[] = "
            function toggleEditor() {
                if(tinyMCE.getInstanceById('post_content') == null) {
                    tinyMCE.execCommand('mceAddControl', false, 'post_content');
                }
                else {
                    tinyMCE.execCommand('mceRemoveControl', false, 'post_content');
                }
            };
        ";

        return parent::_prepareLayout();
    }
}

create a block for the left-side tabs


#app/code/Jeff/SimpleNews/Block/Adminhtml/News/Edit/Tabs.php

<?php
namespace Jeff\SimpleNews\Block\Adminhtml\News\Edit;

use Magento\Backend\Block\Widget\Tabs as WidgetTabs;

/** This file will declare tabs at left column of the editing page */
class Tabs extends WidgetTabs {
    protected function _construct() {
        parent::_construct();
        $this->setId('news_edit_tabs');
        $this->setDestElementId('edit_form');
        $this->setTitle(__('News Information'));
    }

    protected function _beforeToHtml() {
        $this->addTab('news_info',
            [
                'label' => __('General'),
                'title' => __('General'),
                'content' => $this->getLayout()->createBlock('Jeff\SimpleNews\Block\Adminhtml\News\Edit\Tab\Info')->toHtml(),
                'active' => true
            ]
        );

        return parent::_beforeToHtml();
    }
}

Create a block for Form information


#app/cope/Jeff/SimpleNews/Block/Adminhtml/News/Edit/Form.php

<?php
namespace Jeff\SimpleNews\Block\Adminhtml\News\Edit;

use Magento\Backend\Block\Widget\Form\Generic;

/** This file will declare form information */
class Form extends Generic {
    protected function _prepareForm() {
        $form = $this->_formFactory->create(
            [
                'data' => [
                    'id' => 'edit_form',
                    'action' => $this->getData('action'),
                    'method' => 'post'
                ]
            ]
        );

        $form->setUseContainer(true);
        $this->setForm($form);

        return parent::_prepareForm();
    }
}

Create a block to declare the fields for the edit form


#app/code/Jeff/SimpleNews/Block/Adminhmtl/News/Edit/Tab/Info.php

<?php
namespace Jeff\SimpleNews\Block\Adminhtml\News\Edit\Tab;

use Magento\Backend\Block\Widget\Form\Generic;
use Magento\Backend\Block\Widget\Tab\TabInterface;
use Magento\Backend\Block\Template\Context;
use Magento\Framework\Registry;
use Magento\Framework\Data\FormFactory;
use Magento\Cms\Model\Wysiwyg\Config;
use Jeff\SimpleNews\Model\System\Config\Status;

class Info extends Generic implements TabInterface {
    protected $_wysiwygConfig;

    protected $_newsStatus;

    public function __construct(
        Context $context, 
        Registry $registry, 
        FormFactory $formFactory, 
        Config $wysiwygConfig, 
        Status $newsStatus, 
        array $data = [])
    {
        $this->_wysiwygConfig = $wysiwygConfig;
        $this->_newsStatus = $newsStatus;
        parent::__construct($context, $registry, $formFactory, $data);
    }

    protected function _prepareForm() {
        $model = $this->_coreRegistry->registry('simplenews_news');

        $form = $this->_formFactory->create();
        $form->setHtmlIdPrefix('news_');
        $form->setFieldNameSuffix('news');

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

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

        $fieldset->addField('title', 'text', ['name'=>'title', 'label' => __('Title'), 'required' => true ]);
        $fieldset->addField('status', 'select', ['name'=>'status', 'label' => __('Status'), 'options' => $this->_newsStatus->toOptionArray() ]);
        $fieldset->addField('summary', 'textarea', ['name'=>'summary', 'label' => __('Summary'), 'required' => true ]);

        $wysiwygConfig = $this->_wysiwygConfig->getConfig();
        $fieldset->addField('description', 'editor', ['name'=>'description', 'label' => __('Description'), 'required' => true, 'config' => $wysiwygConfig ]);
    
        $data = $model->getData();
        $form->setValues($data);
        $this->setForm($form);

        return parent::_prepareForm();

    }

    public function getTabLabel() {
        return __('News Info');
    }

    public function getTabTitle() {
        return __('News Info');
    }

    public function canShowTab() {
        return true;
    }

    public function isHidden() {
        return false;
    }
}

Create a controller action for create a new News


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/NewAction.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

class NewAction extends News {
    public function execute() {
        $this->_forward('edit');
    }
}

Create Edit Action for the Edit form


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/Edit.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

/** This is the edit action for editing news page */
class Edit extends News {
    public function execute() {
        $newsId = $this->getRequest()->getParam('id');
        $model = $this->_newsFactory->create();

        if($newsId) {
            $model->load($newsId); //just load the id to get the object

            if(!$model->getId()) {
                $this->messageManager->addError(__('This news no longer exists.'));
                $this->_redirect('*/*/');

                return;
            }
        }

        $data = $this->_session->getNewsData(true);
        if(!empty($data)) {
            $model->setData($data);
        }

        $this->_coreRegistry->register('simplenews_news', $model);

        $resultPage = $this->_resultPageFactory->create();
        $resultPage->setActiveMenu('Jeff_SimpleNews::main_menu');
        $resultPage->getConfig()->getTitle()->prepend(__('Simple News'));

        return $resultPage;
    }
}

A Save Action for the edit form


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/Save.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

class Save extends News {
    public function execute() {
        $isPost = $this->getRequest()->getPost();

        if($isPost) {
            $newsModel = $this->_newsFactory->create();
            $newsId = $this->getRequest()->getParam('id');

            if($newsId) {
                $newsModel->load($newsId);
            }

            $formData = $this->getRequest()->getParam('news');
            $newsModel->setData($formData);

            try {
                $newsModel->save();

                $this->messageManager->addSuccess(__('The news has been saved.'));

                /** Check if 'Save and Continue' */
                if($this->getRequest()->getParam('back')) {
                    $this->_redirect('*/*/edit', ['id' => $newsModel->getId(), '_current'=>true]);
                    return;
                }

                //go to grid page
                $this->_redirect('*/*/');
                return;
            }
            catch(\Exception $e) {
                $this->messageManager->addError($e->getMessage());
            }

            //When there are some errors the following codes will execute
            $this->_getSession()->setFormData($formData);
            $this->_redirect('*/*/edit', ['id' => $newsId]);
        }
    }
}

Delete Action for the edit Form


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/Delete.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

class Delete extends News {
    public function execute() {
        $newsId = (int) $this->getRequest()->getParam('id');

        if($newsId) {
            $newsModel = $this->_newsFactory->create();
            $newsModel->load($newsId);


            //Check if this news exists
            if(!$newsModel->getId()) {
                $this->messageManager->addError(__('This news no longer exists.'));
            }
            else {
                try {
                    $newsModel->delete();
                    $this->messageManager->addSuccess(__('The news has been deleted.'));

                    $this->_redirect('*/*/');
                    return;
                }
                catch(\Exception $e) {
                    $this->messageManager->addError($e->getMessage());
                    $this->_redirect('*/*/edit', ['id' => $newsModel->getId()]);
                }
            }
        }
    }
}

The mass delete action the grid list


#app/code/Jeff/SimpleNews/Controller/Adminhtml/News/MassDelete.php

<?php
namespace Jeff\SimpleNews\Controller\Adminhtml\News;

use Jeff\SimpleNews\Controller\Adminhtml\News;

class MassDelete extends News {
    public function execute() {
        $newsIds = $this->getRequest()->getParam('news');

        foreach($newsIds as $newsId) {
            try{
                $newsModel = $this->_newFactory->create();
                $newsModel->load($newsId)->delete();
            }
            catch(\Exception $e) {
                $this->messageManager->addError($e->getMessage());
            }
        }

        if(count($newsIds)) {
            $this->messageManager->addSuccess(__('A total of 1% records were deleted.', count($newsIds)));
        }

        $this->_redirect('*/*/index');
    }
}


This is the end of this tutorial. You now have a full functional module and you can convert this module into blog module for Magento 2 by a little bit tweak.



The screen shots for the backend as following:

Backend Menu and Grid List

magento 2 backend 1

Create new News or Edit a News

magento 2 backend 2

The code can be downloaded at GitHub


comments powered by Disqus