Magento 2.1 – How to Override Blocks in Theme

blocksmagento-2.1theme

I'm trying to override the Topmenu block in Magento 2.1 but can't find any guide to do so. Everything I've found on here and elsewhere either seems to apply only to version 2.0 which appears to use a different folder structure or only has partial code examples which expects me to already know their proper context (which I don't).

My current folder structure for a custom theme is app/design/frontend/Vendor/theme_name. Within this I have the registration, theme, and composer files as well as folders for the various modules, e.g. Magento_Theme and Magento_Search.

From what I understand I need to start with an etc/di.xml file like below, edited from here:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
  <preference for="Magento\Theme\Block\Html\Topmenu" type="[Namespace]\[Module]\Block\Html\Topmenu" />
</config>

I also understand that the next step is to add a Block/Html/Topmenu.php file like the below (again edited from the above source):

namespace [Namespace]\[Module]\Block\Html;

class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{

  protected function _addSubMenu($child, $childLevel, $childrenWrapClass, $limit)
  {

  }

}

However, it's not clear to me what I should use for [Namespace] and [Module], or where to place these files. I've tried using the vendor and theme name, and placing the etc and Block folders in app/design/frontend/Vendor/theme_name, as well as placing them in app/design/frontend/Vendor/theme_name/Magento_Theme, amending the namespaces to Vendor\theme_name\Magento_Theme\Block\Html, but neither have any effect.

If anyone could help explain exactly what I need to do to override the Topmenu block (and by inference any other block) in version 2.1 I would be much appreciated.

Addendum

I've attempted Khoa TruongDinh's answer but it has had no affect. I've used the following files:

app/code/Vendor/MagentoTheme/Block/Html/Topmenu.php

<?php

namespace Vendor\MagentoTheme\Block\Html;

class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{

  protected function _addSubMenu($child, $childLevel, $childrenWrapClass, $limit)
  {

    $html = '';

    if (!$child->hasChildren())
    {
      return $html;
    }

    $colStops = null;

    if ($childLevel == 0 && $limit)
    {
      $colStops = $this->_columnBrake($child->getChildren(), $limit);
    }

    // Added "test" class to test
    $html .= '<ul class="level' . $childLevel . ' test submenu">';
    $html .= $this->_getHtml($child, $childrenWrapClass, $limit, $colStops);
    $html .= '</ul>';

    return $html;

  }

}

app/code/Vendor/MagentoTheme/composer.json

{
    "name": "vendor/magento-theme",
    "description": "",
    "require": {
        "php": "~5.5.0|~5.6.0|~7.0.0",
        "magento/framework": "100.0.*"
    },
    "type": "magento2-module",
    "version": "100.0.1",
    "license": [
        "OSL-3.0",
        "AFL-3.0"
    ],
    "autoload": {
        "files": [ "registration.php" ],
        "psr-4": {
            "Vendor\\MagentoTheme\\": ""
        }
    }
}

app/code/Vendor/MagentoTheme/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
  <preference for="Magento\Theme\Block\Html\Topmenu" type="Vendor\MagentoTheme\Block\Html\Topmenu" />
</config>

app/code/Vendor/MagentoTheme/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="Vendor_MagentoTheme" setup_version="1.0.0"></module>
</config>

app/code/Vendor/MagentoTheme/registration.php

<?php

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

I've then removed the contents of pub/static/frontend, var/generation, and var/view_preprocessed, and flushed the Magento cache. The submenu doesn't have the intended "test" class added:

<ul class="level0 submenu ui-menu ui-widget ui-widget-content ui-corner-all" role="menu" aria-expanded="false" style="display: none; top: 52.6719px; left: 487.5px;" aria-hidden="true">...</ul>

Best Answer

Override block:

Create your own module under app/code folder.
We can use preference to override the class in Magento 2.

app/code/Vendor/Module/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
  <preference for="Magento\Theme\Block\Html\Topmenu" type="Vendor\Module\Block\Html\Topmenu" />
</config>

app/code/Vendor/Module/Block/Html/Topmenu.php

<?php

namespace Vendor\Module\Block\Html;

class Topmenu extends \Magento\Theme\Block\Html\Topmenu
{

    protected function _addSubMenu($child, $childLevel, $childrenWrapClass, $limit)
    {

    }

}

Temporary solution:
Currently, seem that above steps cannot override the the block completely. We need to create new custom theme. And then, create the default.xml file:

app/design/frontend/Vendor/Theme/Magento_Theme/layout/default.xml

<?xml version="1.0"?>

<page layout="3columns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="default_head_blocks"/>
    <referenceBlock name="catalog.topnav" class="Vendor\Module\Block\Html\Topmenu" template="Magento_Theme::html/topmenu.phtml"/>
</page>

It's may be a Magento bug: Are we forced to rewrite a template in Magento2 when rewritting a block?

[EDIT]

1) We can set the template:

app/code/Vendor/Module/Block/Html/Topmenu.php

public function setTemplate($template)
{
    return parent::setTemplate('Vendor_Module::custom-menu.phtml');
}

2) Set template via Xml:

For example:

app/code/Vendor/Module/view/frontend/layout/checkout_cart_index.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="checkout.cart">
            <action method="setTemplate">
                <argument name="template" xsi:type="string">Magento_Checkout::cart.phtml</argument>
            </action>
        </referenceBlock>
    </body>
</page>

Remember to create registration.php and module.xml.

We create the new module because we're overriding the class of Magento. When we want to override any class, we have to create new module.

The custom theme under app/design/frontend contains:
--layout
--templates
--js
--html templates (Knockout templates)
--less, css
--etc...

Read more here and here.

Autoloading standard and naming convention:

For [Namespace] and [Module], we should read more here:

http://www.php-fig.org/psr/psr-0/
http://www.php-fig.org/psr/psr-4/
http://alanstorm.com/magento_2_autoloader_and_class_generation

Related Topic