Creating and deploying Global UDFs and Lucee Extensions
Posted March 29, 2020 by Bobby Hartsfield
The ability to use BIFs (built-in functions) anywhere throughout an application is probably something a lot of people take for granted. You don't stop to think about whether or not you're allowed to use writedump() or writeoutput() in the current location, you just know you can. The ability to extend that confidence to your own custom functions would be fantastic, right?
With Adobe ColdFusion, you have a couple of options (some of which will also work in Lucee):
- You can take advantage of scope searching order and load your own UDFS into one of the globally accessible request level scopes such as URL. Once you've done so, you can access them via "url.myFunction()" or simply "myFunction()". I never liked the idea of bloating the URL scope like that, and you also have to reload request level scopes on each request.
For example, if you had a single CFC with all of your UDF methods, within your Application.cfc pseudo constructor, you could do something like this.
Udfs.cfc
component {
function magicFunction() {
return "Abracadabra!";
}
function anotherMagicFunction() {
return "Open says uhhh... me!";
}
}
Application.cfc
component {
this.name = "udftest";
structAppend(url, new udfs());
}
Index.cfm
<cfscript>
writeDump(url);
writeDump(magicFunction());
writeDump(anotherMagicFunction());
</cfscript>
As you'll see in the dump of the URL scope, the functions are there. You can now reference them by name anywhere in your application; inside CFCs, inside custom tags... anywhere.
This method will work in any version of Adobe ColdFusion Railo or Lucee. - Another great option for ACF is to put your functions in the hidden scope. You are undoubtedly familiar with scopes such as CGI, Server, Form, URL, etc... but what you may not know is that all of those scopes are actually part of another scope which is appropriately deemed "hiddenscope". You can view it's contents by dumping it with the following.
writeDump(getPageContext().getFusionContext().hiddenScope)
To put the same CFC into this scope that we used above, you can simply use the putAll() method.
Application.cfc
component {
this.name = "udftest";
getPageContext().getFusionContext().hiddenScope.putAll(new udfs());
}
Now, when you dump the hiddenscope, you will see all the methods in your CFC and you can use them by name anywhere in your application.
Index.cfm
<cfscript>
writeDump(getPageContext().getFusionContext().hiddenScope);
writeDump(magicFunction());
writeDump(anotherMagicFunction());
</cfscript>
This, in my opinion is much better than overloading a request level scope like URL but it has the same downside of having to be executed on every request. Depending on your UDF library, that is likely not much of a concern but it could grow into one over time and is not an option in Lucee.
While you can load your UDFs into the URL scope the same way in Lucee, there is another, cooler option; you can create your own Lucee extension (lex)! The great thing about Lucee extensions is that it's not limited to functions; you can also create your own tags. Cool, right?
If that isn't enough, you can also have your application automatically deploy your extensions so you don't have to worry about installing it on all your servers (though you can obviously do that as well).
You can read all the specifics about extensions at Lucee.org.
In this example, I will just be creating the same couple of functions created in the previous examples.
First, we will setup the directory structure for the extension.
mkdir ./extensions
mkdir ./extensions/functions
mkdir ./extensions/META-INF
touch ./extensions/META-INF/MANIFEST.MF
touch ./extensions/functions/magicFunction.cfm
touch ./extensions/functions/anotherMagicFunction.cfm
Now to edit the files. First, we need to update the manifest.
./extensions/META-INF/MANIFEST.MF
Manifest-Version: 1.0
id: "d85de2e9-78e8-4927-b8e3e355cc5ff3b1"
version: "1.0.0.0"
name: "Acoderslife"
description: "Custom UDFs to support Lucee applications"
category: "Acoderslife UDFs"
A couple of quick notes:
Now for the actual functions. Nothing too fancy is happening there, they are just basic functions but in CFM files. However, note that each function is a single CFM file with the same name as that function.
./extensions/functions/magicFunction.cfm
<cfscript>
/**
* magicFunction
* @hint returns magical stuff
* @output false
* @version 1.0.0.0
*/
function magicFunction() {
return "Abracadabra!";
}
</cfscript>
./extensions/functions/anotherMagicFunction.cfm
<cfscript>
/**
* anotherMagicFunction
* @hint returns more magical stuff
* @output false
* @version 1.0.0.0
*/
function anotherMagicFunction() {
return "Open says uhhh... me!";
}
</cfscript>
Now that the manifest is done and the functions are in place, we simply need to zip up the functions and META-INF directories. It is important that those two directories are in the root of the zip file. The name of the zip file isn't important, however, you must change the .zip extension to .lex. Once you do that, you have a Lucee extension that you can share or install through the admin extension interface. But, we want to install it automatically so let's get into that.
In order to have Lucee automatically install .lex files, they simply need to be moved into the right place. You can move them there manually and restart Lucee which will pick them up immediately or you can have the application do it for you.
Application.cfc
component {
this.name = "udftest";
deployluceeextensions();
private void function deployluceeextensions(){
//Lucee makes it easy to find the server context location and determine where to put .lex files
local.deployPath = expandPath("{lucee-server}/../deploy");
//Set the path to your extensions directory (where your .lex file is located)
local.extensionsPath = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "..\extensions\");
//If there are any extensions waiting to be deployed, move them to the deploy directory
for(local.lex in directoryList(path=local.extensionsPath, filter="*.lex"))
fileMove(local.lex, local.deployPath)
//Wait for all lex files to be deployed before continuing
while(directoryList(path=local.deployPath, filter="*.lex").len())
sleep(1000);
}
}
Index.cfm
<cfscript>
writeDump(magicFunction());
writeDump(anotherMagicFunction());
</cfscript>
One important note. Your new functions will not exist on the same request that installed them so, when you run the above example the first time, it will error explaining that the magicFunction does not exist. But, if you refresh, you will now see that they work anywhere in your application.
Let's break that down a little. The deployluceeextensions() method is called in the pseudo constructor so it will run on every single request. You could change that in multiple ways. You could put in in onApplicationStart() or even wrap it in a try/catch of your new functions so it only runs when they don't exist. The latter doesn't really lend itself to upgrading extensions though. For example, if version1 exists, it wouldn't deploy version 2 unless you first uninstalled the extension.
On to the deploy function. First, it uses the Lucee path variable "{lucee-server}" to get the server's context path and backs out to the "deploy" directory".
Next, it just sets the extensionsPath to the directory containing your extensions. Mine is one level up from the webroot so "..\extensions" is what was used there.
Next, the function checks the extensions directory for any .lex files. If any are found, they are all MOVED to the "deploy" directory.
Once Lucee deploys a .lex file, it removes it so, finally, the function simply checks every one second until the .lex file is gone (which means it either deployed successfully or errored). If it deployed successfully, you will obviously be able to use your new functions throughout any application under that server context. If it did not deploy successfully, the "deploy" directory will contain a new folder called "failed" (or something similar). That directory will contain your failed .lex file.
In my experience, Lucee picks up new .lex files in the "deploy" directory within 60 seconds and deploys them very quickly. If, for some reason, it does not, the infinite loop will run until the request times out.
Once an extension is installed, you can view or uninstall it through the Lucee admin server context (http://127.0.0.1:
To see a logo on your extension, simply drop a logo.png file in the META-INF directory before creating the lex file.