Saturday 26 June 2021

CFWheels: an even easier way of dealing with all that plugins pollution

G'day:

In the last article ("CFWheels: improving the usability of plugins), I mentioned this part-way through:

although as I type this paragraph, I think I have worked out a better way than what I'm about to show you

Well. In my never-ending quest to provide quantity rather than quality, as soon as I pressed "send" on that, I tried the idea I had. And it worked. An it's so bloody simple.

Go read that other article for full context, but the object of the exercise here is to write CFWheels plug-in implementations so they:

  1. don't pollute every object in the app with all its public functions;
  2. and don't actually require the plugin to be stateless and have only public functions.

I dicked around using closure to solve this. But it's more easily solved by just leveraging… the file system.

I've created another plugin with file structure thus:

../src/plugins/
└── colourPlugin
    ├── ColourPlugin.cfc
    └── implementation
        └── ColourPlugin.cfc

And code thus:

// src/plugins/colourPlugin/ColourPlugin.cfc

import cfmlInDocker.plugins.colourPlugin.implementation.*

component  {

    function init() {
        this.version = "2.2"
        return this
    }

    function ColourPlugin() {
        return new implementation.ColourPlugin()
    }
}
// src/plugins/colourPlugin/implementation/ColourPlugin.cfc

import cfmlInDocker.dao.ColourDao
import cfmlInDocker.models.Colour

component {

    function init() {
        variables.dao = new ColourDao()

        return this
    }

    public function getColourById(id) {
        record = variables.dao.selectColourById(id)
        if (record.recordCount) {
            return new Colour(record.id, record.en, record.mi)
        }
        throw(type="ColourNotFoundException", message="Colour not found", detail="Data for Colour with ID #id# not found")
    }
}

CFWheels only loads the outer CFC as the plugin, so only finds the one public method, so only pollutes everything with that. That method returns an object that actually implements the plugin. Using it in a controller method looks like this:

function testColourPlugin() {
    colourPlugin = ColourPlugin()
    viewVariables = {
        colour = colourPlugin.getColourById(params.id)
    }

    renderView(template="plugin", values=viewVariables)
}

I've tested (see test/functional/plugins/ColourPluginTest.cfc) that CFWheels doesn't load everything recursively, and there's no sign of implementation.ColourPlugin's getColourById method in the pollution space:

import testbox.system.BaseSpec

component extends=BaseSpec {

    function run() {
        describe("Tests for ColourPlugin", () => {
            it("does not pollute the application with implementation methods", () => {
                expect(application.wheels.dispatch).toHaveKey("ColourPlugin")
                expect(application.wheels.dispatch).notToHaveKey("getColourById")
            })
        })
    }
}

This is still more messing around than one ought to have to do in a framework whose job it is to make coding easier, but it's less complicated than the other approaches I took in the previous article. I'd even be happy (-ish) doing CFWheels plugin development this way.

Righto.

--
Adam