6

I want to be able to dynamically write a set of getters and setters in CFML/LUCEE components ( No hardcoded cfproperty tags).

<!--- MyComp.cfc --->
<cfcomponent displayname="MyComp" hint="MyComp" accessors="true">
    <cffunction name="init">
       <cfargument name="dynamicprops" type="array">
       <cfloop array="#dynamicprops#" index="item">
          <!--- 
           Now what? I cannot do a cfsavecontent and write props here.
           It demands cfproperty just after the cfcomponent begins. I 
           tried to do with closures but they are not acually setters 
           and getters. Does anyone know how to better do it? 
          ---> 
      </cfloop>
    </cffunction>
</cfcomponent>

<!--- example call --->
<cfset mc = CreateObject("component","MyComp").init( [{"name"="a","default"=1}] ) />

Then I want to be able to call mc.setA( 100 ) and mc.getA(). But does not happen.

So my humble question is how can I dynamically write setters and getters on component?

PS: Please remeber that I have tried the closure way:

 variables[item.name] = item.default;
 variables["set"&item.name] = function(_val){ variables[item.name] =_val; }
 variables["get"&item.name] = function(){ return variables[item.name; }

Couldn't get worked. How can I do it? Thanks :)

edam
  • 910
  • 10
  • 29

2 Answers2

8

You could use onMissingMethod() for this.

component name="myComponent" hint="myComponent.cfc"{

    function init( struct dynamicProperties={} ){
        dynamicProperties.each( function( name, default ){
            variables[ name ] = default;
        } );
        return this;
    }

    function onMissingMethod( name, args ){
        if( name.Left( 3 ) IS "get" )
            return get( name );
        if( ( name.Left( 3 ) IS "set" ) AND ( args.Len() IS 1 ) )
            return set( name, args[ 1 ] );
        cfthrow( type="NonExistentMethod", message="The method '#name#' doesn't exist" );
    }

    public any function get( required string accessorName ){
        var propertyName = parsePropertyName( accessorName );
        if( !variables.KeyExists( propertyName ) )
            cfthrow( type="NonExistentProperty", message="The property '#propertyName#' doesn't exist" );
        return variables[ propertyName ];
    }

    public void function set( required string accessorName, required any value ){
        var propertyName = parsePropertyName( accessorName );
        if( !variables.KeyExists( propertyName ) )
            cfthrow( type="NonExistentProperty", message="The property '#propertyName#' doesn't exist" );
        variables[ propertyName ] = value;
    }

    private string function parsePropertyName( accessorName ){
        return accessorName.RemoveChars( 1, 3 );
    }

}

Pass it your struct of property names/default values and it will "listen" for getters/setters that match. Any that don't will result in an exception.

<cfscript>
myDynamicProperties = { A: 0, B: 0 }; // this is a struct of names and default values
mc = new myComponent( myDynamicProperties );
mc.setA( 100 );
WriteDump( mc.getA() ); // 100
WriteDump( mc.getB() ); // 0
WriteDump( mc.getC() ); // exception
</cfscript>

UPDATE 1: Property name array replaced with name/default value struct as init argument to allow default values to be set.

UPDATE 2: If you want to pass an array of structs containing your name/default value pairs e.g.

dynamicProperties = [ { name: "A", default: 1 }, { name: "B", default: 2 } ];

then the init() method would be:

function init( array dynamicProperties=[] ){
    dynamicProperties.each( function( item ){
        variables[ item.name ] = item.default;
    } );
    return this;
}

UPDATE 3: If you must use tags and <cfloop> to set your dynamic properties then this is all you need in your init method:

<cfloop array="#dynamicProperties#" item="item">
  <cfset variables[ item.name ] = item.default>
</cfloop>

onMissingMethod won't fire if you try to invoke the dynamic methods as properties like this:

method = mc[ "set#property#" ];
method( value );

Instead just make the set() and get() methods in the component public and invoke them directly:

mc.set( property, value );
mc.get( property );
CfSimplicity
  • 2,338
  • 15
  • 17
  • Sounds awesome, I had that in my mind too, but was lazy to give a try. But you have made it sensible. I will give a try and come back with greetings :) – edam Feb 13 '18 at 09:32
  • Hey, thanks again for a nice answer. Unfortunately I couldn't get it working. I have tried in cfml syntax **not script**. My initiation was like `` . `this.params` serves a set of json encoded param which I desirialize in the init method and set each item as : `` . I think My code was okay but **OnMissingMethod()** has never been triggered as well. I got the typical exceptions not the one we have thrown from **OnMIssingMethod()** – edam Feb 21 '18 at 23:27
  • It sounds like your `params` are actually a **struct** of name/default value pairs rather than an *array* of just the property names. I've edited the init method so it will expect a struct and set the default values. (I'm not clear on why you're serializing and then deserializing those params - probably not relevant to the question). – CfSimplicity Feb 22 '18 at 09:41
  • `params = [ {"name" = "a" , default=1}, {"name" = "b" , default=2} ]` . This is how I pass them to the init method. and set them as `variables[ item.name ] = item.default`. And rest is as you have suggested but the *OnMissingMethod()** does not seem to get called. – edam Feb 22 '18 at 21:30
  • OK. A simple struct is all you need, but if you want to pass in the name/value pairs like that then you'd just need to adjust the `init()` method (see update 2). Without seeing your code I'm not sure why your `onMissingMethod() ` isn't firing. Have you tried my suggested cfc "as is"? – CfSimplicity Feb 23 '18 at 08:35
  • [CFML Exploit onMissingMethod](https://pastebin.com/embed_iframe/vz4D89yn) – edam Feb 24 '18 at 18:32
  • Firstly, your code didn't run because of faulty looping over the params (it's an array not a collection and you don't need to nest another loop - just use a simple array loop). Secondly you're trying to call the dynamic methods by referencing them as properties. `onMissingMethod()` won't fire if you do that. Instead see my suggestion for using the `set()` and `get()` methods directly (don't forget to make those 2 functions public). – CfSimplicity Feb 25 '18 at 09:55
  • Yeah sorry but Actually the paramSet is a collection. `{ "commonParams" = [ {} , {} , {}] , "otherParams" = [ {} , {} , {} ] }` . I showed you the wrong way. Trust me paramSet looping had no problem at all, I know it! *Secondly you're trying to call the dynamic methods by referencing them as properties. * - Yes I suspect that too. I need to do that in *method reference way* actually. – edam Feb 25 '18 at 15:11
  • As I say, just make `get()` and `set()` public and then use those:``. If that's the only way you're going to call them, then you don't need `onMissingMethod()`. – CfSimplicity Feb 25 '18 at 17:14
  • Couldn't solve my problem (and does not matter because there's always alternatives) but I'm so grateful that you have spent so much time trying to help; appreciate that. Thank you very much. – edam Feb 26 '18 at 08:29
3

Consider using accessors

<cfcomponent displayname="MyComp" hint="MyComp" accessors="true">

Source: https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-tags/tags-c/cfcomponent.html

James A Mohler
  • 11,060
  • 15
  • 46
  • 72
  • Sorry, i forgot to use that attribute on cfcomponent. Just edited. In my real code actually had it used. It does not do what I'm looking for. I tried my best to explain what I want. Please reread :) – edam Feb 13 '18 at 00:19
  • If I had to do this, I would put an `array` inside of the object and set and get stuff inside of that. I would not try to patch how objects work, I would just be one level in. Besides you are trying to initialize with an array. Just take that array and load it into an internal array. – James A Mohler Feb 13 '18 at 00:45
  • 1
    I thought and rejected not because it was not a good idea, it was but you may know the thing called **legacy** as you bear it up along with frameworks/design patterns. I'm dealing with legacy and tight sprints! – edam Feb 13 '18 at 01:07
  • And after all, being able to do such things (only if possible!) enables you with more strength as a matter of fact! – edam Feb 13 '18 at 01:08