Custom app directory for UDFs

I was looking through the code base to see if there were any hooks for dynamically registering UDFs that would be available globally in your application and stumbled on this:

https://luceeserver.atlassian.net/browse/LDEV-1995

This was released in 5.3.1.66 and I can find no real reference to it other than the one line about the this.functionPaths setting here: Application.cfc / <cfapplication> :: Lucee Documentation

I’ve just hacked around with it and it works well, though looking at the code it feels like it could/should lead to a bit of an overhead if you have lots of files(?).

What was the motivation behind adding this and is it something that ought to be pushed in terms of documentation and shouting about it (its pretty cool)?

4 Likes

Playing with this, I found it to not perform too well with lots of UDFs, which was not all that surprising.

I went back to the drawing board and created a ticket and PR for application developers to be able to implement UDFs using their own application logic. The implementation I chose matches “standard” cfml approaches so not reinventing the wheel with developer patterns.

Basically, if a developer adds an onMissingFunction( name, args ){} function in their Application.cfc, they can handle missing functions and dynamically call them as with onMissingMethod() approaches.

I’ve tried this out locally and it works really well and has no performance implications that I can find.

https://luceeserver.atlassian.net/browse/LDEV-5094

2 Likes

I hope Lucee will provide this feature.

1 Like

I prefer having my UDFs scoped so it’s always obvious that it’s not a built-in Lucee function or Java method. I put them in the Request scope so they’re accessible from within components. I found that to be surprisingly better performance-wise instead of the Application scope. I have tons of UDF functions with minimal performance impact.

Here’s my udf.cfc which is located in a mapped folder:

/*
How to load this from the app:

Request.udf = new shared.udf();

Then call any method such as:

Request.udf.QueryToArray(qry)
*/

component {
	include "/shared/_cflib_CFMLLibExtras.cfm";
	include "/shared/_cflib_DataManipulationLib.cfm";
	include "/shared/_cflib_DataManipulationLibExtras.cfm";
	include "/shared/_cflib_DateLib.cfm";
	include "/shared/_cflib_DateLibExtras.cfm";
	include "/shared/_cflib_FileSysLibExtras.cfm";
	include "/shared/_cflib_MathLib.cfm";
	include "/shared/_cflib_MathLibExtras.cfm";
	include "/shared/_cflib_NetLibExtras.cfm";
	include "/shared/_cflib_SecurityExtras.cfm";
	include "/shared/_cflib_StrLib.cfm";
	include "/shared/_cflib_StrLibExtras.cfm";
	include "/shared/_cflib_UtilityLib.cfm";
	include "/shared/CronJobFunctions.cfm";
	include "/shared/FunctionsImageMagick.cfm";
}
1 Like

Hi @Dominic_Watson ,
Your finding that this.functionPaths is slow doesn’t surprise me. In fact, I think that the addition of this.functionPaths to Application.cfc amounts to sub-optimal design. It won’t only be slow, I foresee other side-effects in future.

You don’t have to look far to see why. The documentation on this.functionPaths describes it as:

“support for functionPaths in the Application.cfc, so you can create directories containing functions you then can use in your code.”
(bold emphasis is mine)

Here, “you” stands for the user of the application. However, that is an anti-pattern. Application settings are, by definition, global to the application. “You” have no business interfering with Application.cfc. That is the sole prerogative, and responsibility, of the Application. Which is why, any this.Xproperty in Application.cfc can be thought of as the Xproperty of the application.

This is not just a semantic splitting of hairs. It is part of the design at the foundation of the application. But, in this case, highlighting the benefits to the user, has subtly overshadowed a design flaw, as I shall now show.

This.functionPaths is application-global. Let’s say it consists of functionPath1, functionPath2, …, functionPath100. Also, I hope you agree with the statement that Application.cfc is the responsibility of the Application.

Now replace “you” with “the application” in the motivation of this.functionPaths to make it more appropriate. The motivation now reads:

“support for functionPaths in the Application.cfc, so the application can create directories containing functions the application then can use in its code.”

One thing you can say about applications in development is that new functions may be added every second of every minute of every day. So, you may in no time have functionPath101, functionPath102, …, functionPath1000, … This implies that you may have to change this.functionPaths every second. Even more problematic, this makes absurd the notion of this.functionPaths as the functionPaths of the application.

If “functionality” of the user-defined functions is all you need, why engage all that machinery of CFC, includes, instantiation, and so on? You could just define a function directly in request scope.

Here is an example off the top of my head:

/* Alternatively, define these functions in a CFM page, 
and include the page here */
numeric function someFunction1(string arg) {
	return 1;
}
string function someFunction2(string arg) {
	return "abc";
}
struct function someFunction3(string arg) {
	return {a:1,b:2,c:3};
}
request.someFunction1=variables.someFunction1;
writeDump( var="#request.somefunction1#", label="Dump of request.somefunction1");

I use ImageMagick too :scream::grin::sweat_smile:

(Sorry for OT)

1 Like

Because I like organizing functions into files by type and I like the ease and recognition of calling them as Request.udf.functionName(). The udf prefix is actually more important than Request in terms of not confusing with built-in Lucee functions and also prevents collisions with any other potential libraries and objects. And as I stated, the machinery itself has no discernible impact on performance.

We use coldbox and their concept of view + handler helpers which are really great. Having those global helpers straight up available without the framework having to make them available is a performance optimisation and works really well.

Loading the request scope with hundreds of UDF references every request might work well, but is not an improvement really.

There is no necessary antipattern in having functions globally available as if they were BIFs.

1 Like

That should come with a handle-with-care sign, I think. In general, using a user-defined global variable is an anti-pattern in programming.

1 Like

How else are udf functions supposed to be available without potentially clashing with other library namespaces?

That is then the design problem to solve.

Of course, you have to be careful and always handle with care. Just blanket statements around “x is bad” should also be handled with care. It is of course up to the user to use a feature wisely.

For some, Lucee BIFs are an antipattern. For others, Lucee is an antipattern. But it is how the language works. If a development team wishes to extend the BIF library with their own essential functions, then we should let them imo. That team may decide to prefix their UDFs with $ or some other signifier, but it is not for the language to be prescriptive.

2 Likes