Search code examples
coldfusionmixinscfccoldfusion-2016

Call ColdFusion method with explicit receiver


I'm trying to write a mixin in ColdFusion.

ExampleMixin.cfc:

component {
    remote void function mixin(component, methodName) {
        var original = component[methodName];
        component[methodName] = function() {
            writeOutput("Mixin!");
            return original(arguments);
        };
    }
}

test.cfc:

component {
    new ExampleMixin().mixin(this, 'foo');

    remote string function foo() {
        return getOutput();
    }

    private string function getOutput() {
        return "Hello, World!";
    }
}

Running foo produces an error, Variable GETOUTPUT is undefined.. If I comment out new ExampleMixin().mixin(this, 'foo');, it runs fine.

It looks like when foo is run from the wrapper, it's not running in the right context. In JavaScript, one would write foo.call(component, ...arguments) to rectify this. Is there an equivalent in ColdFusion?


Solution

  • ColdFusion uses both the this and variables scopes for storing function references. The reference that's used depends how the function is invoked. If the function is invoked from a sibling, the variables reference is used. If the function is being invoked externally, then the this reference is used.

    The following code uses a base class to supply the mixin functionality. The $mixin function takes a component instance and injects all of its functions. If there's a name collision a wrapper will call the mixin first, then the original function. I'm generating new function names for both the original and mixin functions so references can be set in both scopes.

    This was tested on Lucee 5.2.8.50.

    mixable.cfc

    component {
        function $mixin(obj) {
            var meta = getComponentMetadata(obj);
    
            for(var func in meta.functions) {
                if(structKeyExists(this, func.name)) {
                    var orig = func.name & replace(createUUID(), '-', '', 'all');
                    var injected = func.name & replace(createUUID(), '-', '', 'all');
    
                    this[orig] = this[func.name];
                    variables[orig] = this[func.name];
    
                    this[injected] = obj[func.name];
                    variables[injected] = obj[func.name];
    
                    var wrapper = function() {
                        this[injected](argumentCollection=arguments);
                        return this[orig](argumentCollection=arguments);
                    };
                    this[func.name] = wrapper;
                    variables[func.name] = wrapper;
                } else {
                    this[func.name] = obj[func.name];
                    return variables[func.name] = obj[func.name];
                }
            }
        }
    }
    

    test.cfc

    component extends="mixable" {
        remote function foo() {
            writeOutput("foo(), calling bar()<br>");
            bar();
        }
    
        private function bar() {
            writeOutput("bar()<br>");
        }
    }
    

    mixin.cfc

    component {
        function foo() {
            writeOutput("foo mixin, calling bar()<br>");
            bar();
        }
    
        function myfunc() {
            writeOutput("myfunc()<br>");
        }
    }
    

    index.cfm

    <cfscript>
    t = new test();
    t.$mixin(new mixin());
    t.myfunc();
    t.foo();
    </cfscript>
    

    Output

    myfunc()
    foo mixin, calling bar()
    bar()
    foo(), calling bar()
    bar()