[ScreenCast] How To Change Core Functionality in Magento2

Prefer a ScreenCast?


Introduction

Today we’ll learn how would you go about changing functionality in Magento2 in a proper way, because that’s something you do every single day as a Magento developer, while customising the platform for your clients.

The first important thing is: you don’t need to modify the original code in order to change its behavior. It’s considered as a terrible idea actually, because you won’t be able to upgrade the system later, and you create a big mess in your application where you don’t even know what’s original code and what’s added, so you should always avoid that approach.

Instead, similarly to Magento 1, Magento2 allows you to change, or extend, the behavior of any original method in the codebase, without even touching it, and that’s exactly what we’re going to explore in this lesson.

Creating a Calculator Module

So why don’t we start by creating a new module – we’ve already seen how to do that it in the previous video, let’s see how quickly we can do this again.

MageClass_Calculator, the module structure

MageClass_Calculator, the module structure

So because we don’t have the concept of codepools in Magento2 anymore, we’re just going to create a module right inside the app/code folder. MageClass is the module vendor, and the module itself we’ll be called Calculator. And we’ll need to create etc folder and a file called module.xml and this file is going to register our module.

<?xml version="1.0"?>
<config>
    <module name="MageClass_Calculator" setup_version="2.0.0"/>
</config>

And we need to update app/etc/config.php to enable this module

'MageClass_Calculator' => 1

Let’s make sure the cache is cleared because we made a configuration change.

Open up Basic.php and paste the following:

<?php

namespace MageClass\Calculator\Model;

class Basic {

    public function divide($x, $y)
    {
        return $x / $y;
    }
}

This is the class that we’ll be changing. It has a namespace declared at the top and a simple function which divides two numbers and returns a result. I think it’s better to keep it as basic as possible, at least at first, while we’re focusing on other things.

Ok, now in order to test this, I like to initialise Magento in a stand alone script, because that way we won’t need to worry about creating a route and a controller. So create test.php file in your Magento root and paste the following code:

<?php
require __DIR__ . '/app/bootstrap.php';

class TestApp extends \Magento\Framework\App\Http implements \Magento\Framework\AppInterface
{  
    public function launch()
    {
    	$calculator = $this->_objectManager->create('\MageClass\Calculator\Model\Basic');
    	echo $calculator->divide(10,0);        
        return $this->_response;
    }

    public function catchException(\Magento\Framework\App\Bootstrap $bootstrap, \Exception $exception)
    {
        return false;
    }
}

$bootstrap = \Magento\Framework\App\Bootstrap::create(BP, $_SERVER);
$app = $bootstrap->createApplication('TestApp');
$bootstrap->run($app);

We asked the object manager to instantiate Basic class for us with two arguments: 10 and 2.

If we run test.php in the browser, “5” should appear on the screen. Now let’s think of this class as something that we are not allowed to change directly, but we are asked to fix. For example, if we pass a zero as the second argument, PHP will throw an exception because division by zero is not possible. Or if there’s anything else that this method should do, we need to find a way to extend it in a safe way.

Plugins In Magento2

That’s definitely possible. Magneto2 introduced a new concept called plugins, which allow you to hook into a specific events that occur in any public method life-cycle. Any public method has before, around and after events, or think of them as spots that we can hook into. For example, what if we want to add a specific bit of code when a product is saved to the database, or what if in our example, we want to check if $y is 0 before we allow PHP to execute the rest of the method? Or what if we want to add some behavior before and after an original method is called? This is all possible by default, the system provides these hooks out of the box, you don’t need to dispatch these events manually, like we did in Magento1.

Exploring “Before” Event

To demonstrate how powerful this is, open up di.xml and update like this:

<?xml version="1.0" ?>
<config>
	<type name="MageClass\Calculator\Model\Basic">
		<plugin name="MageClass_Calculator::before" type="MageClass\Calculator\Plugin\Before" sortOrder="1" />
	</type>
</config>
  • name identifies a plug-in. This may be whatever name you like
  • type corresponds to our class that will observe the original class
  • sortOrder determines the order in which plug-ins that call the same method are run. If you have two Before plugins that rewrite the same class the sortOrder attribute will determine which one goes first.

The file Before.php should look like this:

<?php

namespace MageClass\Calculator\Plugin;

class Before
{
	public function beforeDivide($calculator, $x, $y)
	{
		echo 'Hello from before plugin <br />';
	}
}

Notice that beforeDivide method has access to all public methods of the original class through the first parameter called $calculator, and what comes next are the original parameters of the divide method, $x and $y.

Make sure to delete var folder and refresh the page, and test this in the browser.

What we can also do in before-listener is to change the arguments that will be passed to the original method. For example, we can say:

public function beforeDivide($calculator, $x, $y)
{
	echo 'Hello from before plugin <br />';
	return [5,10];
}

We’re returning an array of two elements, because the original method receives 2 parameters. Remember that what we return in beforeDivide is going to be passed to divide and since it expects two inputs, we have to return two inputs as well.

But, notice that if we pass only one argument, it’s going to fail because the argument two will be missing.

So before Event can come in handy when we want to do something, maybe perform some checks before the original method and based on that maybe change values that are passed. This example is silly of course, but I am sure it has some better use case.

Exploring “After” Event

Now let’s see what after-listener can do for us. If we see the output in the browser, it’s pretty simple, without any message – just a result. So why don’t we wait for divide method to return the result and then hook into that event to provide some friendly message.

Okay, first we’re going to update the di.xml:

<?xml version="1.0" ?>
<config>
	<type name="MageClass\Calculator\Model\Basic">
		<plugin name="MageClass_Calculator::before" type="MageClass\Calculator\Plugin\Before" sortOrder="1" />
		<plugin name="MageClass_Calculator::after" type="MageClass\Calculator\Plugin\After" sortOrder="1" />
	</type>
</config>

And we’re going to update After.php file:

<?php

namespace MageClass\Calculator\Plugin;

class After
{
	public function afterDivide($calculator, $result)
	{
		echo 'The result is: ' . $result;
	}
}

Okay, now, it has a slightly different declaration – it still receives the calculator object, but the second argument will be the output of the divide method, so whatever divide returns, is going to be passed.

After this step, we need to clear the cache and try in the browser.

Exploring “Around” Event

Now let’s explore the last event option called around. It’s probably the most useful event, because you can change both the arguments and returned values of an original method and also add some behavior before and after an original method is called. You really have the full control on the execution flow of the original method. Let’s demonstrate how powerful it is for fixing that “Division by zero” exception.

We’ll start off by commenting out before and after plugins in di.xml and let’s have only around this time.

<?xml version="1.0" ?>
<config>
	<type name="MageClass\Calculator\Model\Basic">
		<plugin name="MageClass_Calculator::around" type="MageClass\Calculator\Plugin\Around" sortOrder="1" />
	</type>
</config>

Okay, now let’s update the file Around.php:

<?php

namespace MageClass\Calculator\Plugin;

class Around
{
	public function aroundDivide($calculator, $divide, $x, $y)
	{
		if($y == 0)
		{
			return 'Unable to divide by 0';
		}

		$result = $divide($x, $y);
		return 'The result is: '. $result;
	}
}

This method, aroundDivide receives many arguments: $calculator (same as with before and after), next is the original, divide method itself and what comes after are the arguments of the divide method, $x and $y.

So you see how many options you have here (explained better in screencast), and that’s why I guess the around method is the best choice when you want to make some bigger modifications to the original method.

We want to test both cases:

  1. non-zero $y: (expected to return the result)
  2. zero $y (expected Unable to divide by 0).

That’s pretty it when it comes to plugin usage in Magento2. I recommend you to visit Magento Devdocs and review Magento Plugins section.

Overriding Non-Public Methods Using Preferences

Now, to close this lesson, I’d like to show you a different approach of overriding existing functionality which is a bit more familiar to Magento1 developers, because it allows you to tell the object manager, through the xml configuration, that whenever it’s asked to instantiate a specific class, it should actually instantiate some other class. So it’s not event based like plugins, we’re talking about very familiar rewriting system, so let quickly see how would we go about setting it up.

Switch back to di.xml and make sure it looks like this:

<?xml version="1.0" ?>
<config>
	<preference for="MageClass\Calculator\Model\Basic" type="MageClass\Calculator\Model\Advanced" />
</config>

so here we told Magento that anytime someone asks the object manager to instantiate a MageClass\Calculator\Model\Basic, it should actually instantiate a MageClass\Calculator\Model\Advanced. So, we need to update Advanced.php:

<?php

namespace MageClass\Calculator\Model;

class Advanced {

    public function divide($x, $y)
    {
        return 'REWRITTEN';
    }
}

After you clear the cache, check if REWRITTEN has appeared in the browser.

Generally, this can be very useful in situations when you want to rewrite a non public method, because  plugins can work with public methods only – you will not be able to slot some functionality before, around or after a protected or private method, keep that in mind.

However, preferences in Magento have a specific role and that is to encourage developers to use interfaces and service contracts design. This is a different topic, but just as an intro, if we pick into etc/di.xml file, which comes by default with Magento installation, we can see interfaces everywhere on the left side and everything on the right represent the concrete class that we’ll be used when an interface on the left is required.

So whenever you see Psr\Log\LoggerInterface declared as a dependency in a class constructor, Magento object manager will instantiate Magento\Framework\Logger\Monolog unless the configuration from etc/di.xml is not changed by some module in the system, which is possible too.

Conclusion

All right, so to recap – when you want to rewrite public methods, create a plugin, otherwise, I guess preferences are the only option.

Milan Stojanov is a certified Magento developer and Tuts+ author on Magento Fundamentals premium course. He likes to read and write about web development.

  • Michael Rivet

    Thank you, very useful post !

  • Ratnesh Singh

    Very nice post

  • Matt

    I want to save additional fields is it possible to use a Plugin to run an after even on the function _afterSave in MagentoBundleModelResourceModelOption? Could you provide an example please?

  • Dev Kamal

    I got this error

    Catchable fatal error: Argument 2 passed to MagentoFrameworkAppHttp::__construct() must be an instance of MagentoFrameworkEventManager, none given, called in /home/kamaldev/workspace/luma/vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php on line 93 and defined in /home/kamaldev/workspace/luma/vendor/magento/framework/App/Http.php on line 87

    • chollie

      Hi man this is an issue of generation..deelte the `var/generation` folder so your dependencies injected are updated

  • chollie

    Hmmm I am having difficulty rewriting the class but extending the original class. Overriding a single private method, it is not called.

  • Deepak Shinde

    I am getting this error “Fatal error: Call to a member function getStore() on null in …vendormagentomodule-customerModelAccountManagement.php on line 406” after override customer ForgotPasswordPost Controller method using preferences.