Categories


Archives


Recent Posts


Categories


The Curious Case of Magento 2 Mixins

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

We’re back, with an unexpected fourth article in our Advanced Javascript series.

At the end of our UI Component series, we said

The same applies to system integrators, but if you’re the one building and maintaining a specific Magento instance I’d say it’s safe to also use RequireJS’s selective “monkey patching” abilities to modify the behavior of core javascript objects. Keep an eye on Magento Quickies for an upcoming article on how to do this with Magento’s special requirejs-config.js files.

However, when I started researching RequireJS monkey patching and the best way to apply it in Magento 2, I was in for a bit of a surprise. Joe Constant pointed me towards a feature Magento 2 erroneously calls mixins, and all sorts of confusion resulted.

Our end goal today is to teach you a technique for applying a safe method rewrite technique using Magento 2 javascript techniques — one that goes beyond the usual RequireJS tools. To get there though, we’ll need to wade through some very puzzling choices made by Magento’s product and engineering teams.

We’re going to start with a quick programming lesson, continue with some related feature highlights of Magento’s javascript system, and then fall into discussing Magento’s weirdly named mixins system.

What is a Mixin?

A “mixin” is, from one point of view, an alternative to traditional class inheritance. Mixins date back to lisp programming of the early/mid 1980s.

In “classical OOP”, you might define three classes like this

class A
{
    public function foo()
    {
    }
}

class B extends A
{
    public function bar()
    {
    }
}

class C extends B
{
    public function baz()
    {
    }
}

$object = new C;

This hierarchy chain means your instantiated $object will have a baz method, a bar method, and a foo method.

Mixins offer a different approach. With mixins, you get to pick and choose where you class’s (or object’s) method(s) come from. With an imaginary mixin based language, the above might look like

class A
{
    public function foo()
    {
    }
}

class B
{
    public function bar()
    {
    }
}

class C
{
    mixin A;
    mixin B;

    public function baz()
    {
    }
}
$object = new C;

Notice no classes inherit from one another. Instead, the programmer indicates that class C should get methods from class A and class B. The result is more flexibility, at the expense of some ambiguity about how conflicts between different mixins should work, or what the language syntax should be for specifying mixins.

If you’ve ever used PHP Traits, you’ve used a simplified mixin system. In PHP, you need to define explicit traits, and then can combine those traits in your classes. Traits, by themselves, can’t be instantiated as objects. PHP classes, by themselves, can’t be used as traits.

Contrast this with ruby, which allows one module to completely “include” (or “mix in”) another module’s methods.

You’ll also see the idea of multiple inheritance thrown about in mixin discussions. With multiple inheritance, classes still extend other classes, but you’re allowed to have a single class extend more than one class.

Javascript and Mixins, Sitting in a Tree

Unlike classes, there doesn’t seem to be a consensus on how mixin syntax should work across languages. Some languages have explicit mixins while other languages have de-facto mixins due to the nature of their object system.

Javascript is an example of the later. Javascript doesn’t have any native classes. In javascript, you define methods by attaching functions to objects

var foo = {};

foo.someMethod = function(){
    //...
};

Since objects can be easily reflected into, Javascript is a fertile enviornment for developers who want to build systems for creating mixin like objects. One library that offers this sort of functionality (although the word mixin isn’t used) is underscore.js.

Using the extend method in underscore.js, you can have a de-facto mixin-like behavior. Consider the following

var a = {
    foo:function(){
        //...
    }
};

var b = {
    bar:function(){
        //...
    }

}

c = _.extend(a, b);    

The object in c will end up having both a foo, and a bar method. The _.extend method lets us say

Hey, javascript, create a new object with stuff from these other objects

Confusingly, underscore.js has an actual method named mixin, but this method is for adding methods to the underscore JS object itself. Something something cache invalidation and naming things.

Magento uiClass Objects

If you’ve worked your way through the UI Component series, you’re already familiar with Magento’s uiClass objects. These objects also have an extend method. This method looks similar to the underscore.js method

var b = {
    bar:function(){
        //...
    }

}
UiClass = requirejs('uiClass');

// class NewClass extends uiClass
var NewClass = UiClass.extend(b);

// class AnotherNewClass extends NewClass
var AnotherNewClass = NewClass.extend({});

var object = new NewClass;
object.bar();

However, the uiClass extend method is used for something slightly different. The purpose of the uiClass.extend is to create a new javascript constructor function that’s based on an existing javascript constructor function. Above, NewClass won’t get a bar method, but objects instantiated from it will.

While this feels more like straight inheritance, there might be some folks who would call this a mixin due to the uiClass‘s implementation details.

We’re now going to jump to a completely different topic, but keep all of the above in mind.

Magento 2 RequireJS Mixins

Magento 2’s requirejs-config.js files have a feature that’s labeled as a “mixin”. This feature has (almost) nothing to do with traditional computer science mixins, so we’ll continue to refer to them with the “skeptical quotes”.

Despite all the shade we’re throwing at the feature’s name, it’s actually a very good and important feature. A Magento 2 RequireJS “mixin” allows you to programmatically listen for the initial instantiation of any RequireJS module and manipulate that module before returning it.

If that didn’t make sense, a quick sample module should make things clearer. First, create and enable a blank module with the following pestle commands (or use your own module creating methodology).

$ pestle.phar generate_module Pulsestorm RequireJsRewrite 0.0.1
$ php bin/magento module:enable Pulsestorm_RequireJsRewrite
$ php bin/magento setup:upgrade    

If you’re interested in creating a module by hand, or curious what the above pestle command is actually doing, take a look at our Introduction to Magento 2 — No More MVC article.

Once that’s complete, create the following requirejs-config.js file

//File: app/code/Pulsestorm/RequireJsRewrite/view/base/requirejs-config.js
var config = {
    'config':{
        'mixins': {
            'Magento_Customer/js/view/customer': {
                'Pulsestorm_RequireJsRewrite/hook':true
            }
        }
    }
};    

and the following RequireJS module/file.

//File: app/code/Pulsestorm/RequireJsRewrite/view/base/web/hook.js
define([], function(){
    'use strict';    
    console.log("Called this Hook.");
    return function(targetModule){
        targetModule.crazyPropertyAddedHere = 'yes';
        return targetModule;
    };
});

With the above in place, if you load the Magento homepage (or any page which uses the Magento_Customer/js/view/customer RequireJS module) you should see the

Called this Hook   

message output to your javascript console. Also, if you examine the Magento_Customer/js/view/customer module via the console, you’ll see it has an extra crazyPropertyAddedHere property

> module = requirejs('Magento_Customer/js/view/customer');
> console.log(module.crazyPropertyAddedHere)
"yes"

With the above code, we changed the object returned by the Magento_Customer/js/view/customer module. If used judiciously, this is an incredibly powerful feature.

What Just Happened?

Before we jump to the implications of this feature, lets talk about the code we just wrote.

//File: app/code/Pulsestorm/RequireJsRewrite/view/base/requirejs-config.js
var config = {
    'config':{
        'mixins': {
            'Magento_Customer/js/view/customer': {
                'Pulsestorm_RequireJsRewrite/hook.js':true
            }
        }
    }
};    

If you’re not familiar with these requirejs-config.js files, they allow individual Magento modules to provide configuration values for RequireJS. You can read more about them in our Magento 2 and RequireJS article.

If you are familiar with them, you may be confused by the mixins configuration key. This is not a part of standard RequireJS. This is a special configuration flag Magento introduced to their RequireJS system. Don’t let the word mixins confused you. As we previously mentioned, this has (almost) nothing to do with the programming concept we discussed earlier. It’s just a poorly chosen name.

The mixins property should be an object of key values pairs. The key (Magento_Customer/js/view/customer) is the object whose creation you want to listen for. The value is another object

{
    'Pulsestorm_RequireJsRewrite/hook':true
}

The key of this second object is the RequireJS module that’s going to be listening. We named this module hook, but that’s not required. You can use any module name you like here.

Next, we defined our RequireJS module.

//File: app/code/Pulsestorm/RequireJsRewrite/view/base/web/hook.js
define([], function(){
    'use strict';    
    console.log("Called this Hook.");
    return function(targetModule){
        targetModule.crazyPropertyAddedHere = 'yes';
        return targetModule;
    };
});

If you’re not familiar with how Magento resolves RequireJS module names to URLs, you may want to read through the front end articles in our Magento 2 for PHP MVC Developers series.

These “listener/hook” modules are standard RequireJS modules. They should return a callable object (i.e. a javascript function). This is the function Magento will call after loading a RequireJS module. It has a single parameter (targetModule) above. This variable will be a reference to whatever the spied on RequireJS module (Magento_Customer/js/view/customer in our example) returns.

Whatever this callback function returns will be treated by the rest of the system as the actual module. That’s why Magento_Customer/js/view/customer had our extra crazyPropertyAddedHere property.

Class Rewrites for Javascript

As we’ve mentioned a few times, this is an incredibly powerful feature. One thing you could do with it os replace method implementations on RequireJS modules that return objects

define([], function(){
    'use strict';    
    console.log("Called this Hook.");
    return function(targetModule){
        targetModule.someMethod = function(){
            //replacement for `someMethod
        }
        return targetModule;
    };
});

Also, if the module in question returns a uiClass based object? You could use uiClass‘s extend method to return a different class that extended the method, but used uiClass‘s _super() feature to call the parent method.

define([], function(){
    'use strict';    
    console.log("Called this Hook.");
    return function(targetModule){
        //if targetModule is a uiClass based object
        return targetModule.extend({
            someMethod:function()
            {
                var result = this._super(); //call parent method

                //do your new stuff

                return result;
            }
        });
    };
});

These are powerful techniques that allow a careful developer, with a small amount of code, to change existing system behavior in exactly the way she wants to. However, like class rewrites in Magento 1, and the class <preference> feature in Magento 2, the above examples are a winner take all situation. While multiple developers can all safely setup their own hooks, they can’t all redefine the same method or function. One person’s modifications will win out over the other person’s.

Fortunately, Magento 2 has a solution for that in the mage/utils/wrapper module.

Wrapping Function Calls

The mage/utils/wrapper module allows for functionality similar to a Magento 2 backend around plugin. Here’s a simple example that should demonstrate what it means to “wrap” a function.

var example = {};
example.foo = function (){
    console.log("Called foo");
}

var wrapper = requirejs('mage/utils/wrapper');

var wrappedFunction = wrapper.wrap(example.foo, function(originalFunction){        
    console.log("Before");
    originalFunction();
    console.log("After");
});

//call wrapped function
wrappedFunction();

//change method definition to use wrapped function
example.foo = wrappedFunction;

If you run the above code, you’ll see the following output

Before
Called foo
After

The wrap method accepts two arguments. The first is the original function you want to wrap. The second is the function you want to wrap it with. The originalFunction parameter will be a reference to the function you’re trying to wrap (example.foo above). The wrap method returns a function that, when called, will call your function. Your function can, if you desire, call the original function.

The point of the wrapper module is to wrap an existing function call with new code without needing to edit the original function. It’s another powerful technique that javascript’s flexible object system enables. Another great thing about wrapping is, multiple people can do it. Try running the following after running the above code.

var wrappedFunction2 = wrapper.wrap(wrappedFunction, function(originalFunction){        
    console.log("Before 2");
    originalFunction();
    console.log("After 2");
});   

wrappedFunction2();

This means if you’re using it to replace method definitions, you can avoid the winner take all situation we described earlier. Consider the following “mixin” hook.

define(['mage/utils/wrapper'], function(wrapper){
    'use strict';    
    console.log("Called this Hook.");
    return function(targetModule){

        var newFunction = targetModule.someFunction;
        var newFunction = wrapper.wrap(newFunction, function(original){
            //do extra stuff

            //call original method
            var result = original();    

            //do extra stuff                

            //return original value
            return result;
        });

        targetModule.someFunction = newFunction;
        return targetModule;
    };
});    

Here we’ve replaced the definition of someFunction with our wrapped function. This technique also has the advantage of working with RequireJS modules that return functions instead of objects.

Why Call this a Mixin

All this still leaves the question of why Magento calls this feature a mixin. A developer can certainly use this feature to implement mixin like behavior, but the feature itself is more of a listener/hook and has nothing to do with adding methods to objects.

I’m sure, to a non-technical user, all this terminology seems like interchangeable jargon but — words still mean things. When a junior, or even intermediate developer, encounters this feature they’re going to expect some sort of real mixin functionality. They’ll spend hours, possibly days, spinning their wheels trying to make it work until they give up.

When a senior developer encounters this they’re going to wonder why something a simple peer/code-review should have caught made it through engineering, through product, and into a production system. Like so much of Magento 2, it feels like we’re looking at the skeleton of a great new building, but being sold office space.

Regardless, this not really a mixin functionality is powerful, and is the perfect mechanism for changing your system’s behavior. As an extension developer, it’s still a crap shoot as to whether a particular RequireJS module will still be around version to version, but for system integrators who own a particular Magento system (and can adjust customizations over time) not really a mixin customizations will be a boon.

Series Navigation<< Magento 2: KnockoutJS IntegrationKnockout Observables for Javascript Programmers >>

Copyright © Alan Storm 1975 – 2017 All Rights Reserved

Originally Posted: 27th October 2016