How to Dynamically write implicit getters and setters in ColdFusion/Lucee Components?
Asked Answered
P

2

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 :)

Prejudice answered 12/2, 2018 at 23:28 Comment(0)
S
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 );
Sperm answered 13/2, 2018 at 9:13 Comment(10)
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 :)Prejudice
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 <cfset var param = CreateObject("component", "my_param_comp").init( SerializeJSON( this.params ) ) /> . this.params serves a set of json encoded param which I desirialize in the init method and set each item as : <cfset variables[item.name] = item.default /> . 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()Prejudice
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).Sperm
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.Prejudice
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"?Sperm
CFML Exploit onMissingMethodPrejudice
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).Sperm
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.Prejudice
As I say, just make get() and set() public and then use those:<cfset paramObj.set( item.name, data[ item.name ] ) /><cfset value = paramObj.get( item.name )>. If that's the only way you're going to call them, then you don't need onMissingMethod().Sperm
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.Prejudice
H
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

Hearty answered 12/2, 2018 at 23:33 Comment(4)
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 :)Prejudice
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.Hearty
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!Prejudice
And after all, being able to do such things (only if possible!) enables you with more strength as a matter of fact!Prejudice

© 2022 - 2024 — McMap. All rights reserved.