Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Tips for Writing Modules for All Frameworks (But Especially for ColdBox)

Tips for Writing Modules for All Frameworks (But Especially for ColdBox)

Eric Peterson

April 27, 2018
Tweet

More Decks by Eric Peterson

Other Decks in Technology

Transcript

  1. What this talk isn't: → How to add ColdBox module

    magic to other frameworks. → Anything to do with UI modules. → Suggestions on what modules to write.
  2. What this talk is: → How to build awesome modules

    for the entire CFML community to use. → How to do it without losing all of the special sauce that makes ColdBox awesome.
  3. Who Am I? Eric Peterson ! Utah " Ortus #

    ForgeBox, ColdBox Elixir $ Prolific Module Author % 1 wife, 2 kids, 1 dog
  4. Quick Overview of Modules → Reusable packages of functionality. →

    Semantically Versioned → Can depend on other modules → Autoload functionality (in ColdBox)
  5. Examples of modules you don't want to write yourself →

    qb — a CFML query builder → cfcollection — Functional array programming → cbmarkdown — Markdown parsing → str — String utility library
  6. What does coupling to ColdBox look like? component singleton {

    property name="interceptorService" inject="coldbox:interceptorService"; property name="wirebox" inject="wirebox"; function load( mapping ) { interceptorService.processState( "preLoad", { "mapping" = mapping } ); var instance = wirebox.getInstance( mapping ); interceptorService.processState( "postLoad", { "instance" = instance "mapping" = mapping, } ); return instance; } }
  7. With just a few changes, this component can be used

    in any framework (or no framework).
  8. Sneak Peak component { property name="interceptorService"; property name="DIEngine"; function init(

    DIEngine, interceptorService = new NullInterceptorService() ) { variables.DIEngine = arguments.DIEngine; variables.interceptorService = arguments.interceptorService; return this; } function load( mapping ) { interceptorService.processState( "preLoad", { "mapping" = mapping } ); var instance = DIEngine.getInstance( mapping ); interceptorService.processState( "postLoad", { "instance" = instance "mapping" = mapping, } ); return instance; } }
  9. If you have dependencies, you will need to document how

    they are used. We'll talk about dependency injection later.
  10. But what about...? → WireBox → Interceptors → Testing →

    All the other cool stuff that ColdBox does for me that I don't want to recreate?
  11. For the most part, your annotations will work just fine

    in ColdBox and do nothing in other frameworks. This could be perfectly fine. component { property name="grammar" inject="DefaultGrammar@qb"; function get( options ) { return grammar.runQuery( toSQL() ); } }
  12. Yes, we know they should be, but how about we

    help bring them in to the future instead of just chiding them?
  13. Instead of property injection, let's use constructor injection... component {

    property name="grammar"; /** * @grammar.inject DefaultGrammar@qb */ function init( grammar ) { variables.grammar = arguments.grammar; } function get( options ) { return grammar.runQuery( toSQL() ); } }
  14. ...or property injection plus explicit setters. // Setters here dynamically

    created by accessors="true" component accessors="true" { property name="grammar" inject="DefaultGrammar@qb"; function get( options ) { return grammar.runQuery( toSQL() ); } }
  15. Make sure to document it! <!-- README.md --> ## Not

    using ColdBox? To create a `QueryBuilder`, you will need to pass in your desired grammar to the constructor. (You can always change this later by calling the `setGrammar` method and passing in a new grammar.)
  16. component { property name="grammar"; /** * @grammar.inject DefaultGrammar@qb */ function

    init( grammar ) { variables.grammar = arguments.grammar; } function get( options ) { return grammar.runQuery( toSQL() ); } }
  17. component { function configure() { binder.map( "QueryBuilder@qb" ) .to( "#moduleMapping#.models.QueryBuilder"

    ) .initArg( name = "defaultGrammar", ref = "DefaultGrammar@qb" ); } } }
  18. component { function onMissingMethod( missingMethodName, missingMethodArguments ) { var req

    = new Hyper.models.HyperRequest() return invoke( req, missingMethodName, missingMethodArguments ); } }
  19. Then, in your code, use DIEngine: component { property name="DIEngine";

    property name="entityMapping" function newEntity() { return DIEngine.getInstance( entityMapping ); } }
  20. And in your ModuleConfig.cfc, set up WireBox as the default

    for ColdBox apps: component { function configure() { binder.map( "MyComponent@MyModule" ) .to( "#moduleMapping#.models.MyComponent" ) .initArg( name = "DIEngine", dsl = "wirebox" ) } }
  21. You don't need to actually type-hint this interface — you

    just have to document it in your README: <!-- README.md --> ## Not using ColdBox? Make sure to bring your own DIEngine. It needs to conform to the following interface: interface name="DIEngine" { function getInstance( string name, string dsl, struct initArguments ); }
  22. Other Gotchas → Avoid onDIComplete methods in favor of something

    more explicit. → Remember that going the old-fashioned constructor route means that everything needs to be injected. → On the flip side, remember that if you are expecting or needing DI, you need to use a DI engine.
  23. component accessors="true" { property name="interceptorService"; function run() { if (

    ! isNull( interceptorService ) ) { interceptorService.processState(); } } }
  24. component { property name="interceptorService" function init( interceptorService = new NullInterceptorService()

    ) { variables.interceptorService = arguments.interceptorService; return this; } function run() { interceptorService.processState( "preRun", getData() ); // ... } }
  25. You can always include an integration test with ColdBox as

    well. In fact, this is a good idea to make sure you're ModuleConfig.cfc is configured correctly.
  26. Community Members, remember... Most modules are just a few lines

    of config away from being perfect for your project
  27. → Started with ColdBox in mind. → Decided to use

    plain old new for the composition → Tony Junkes wrote a blog post about integrating with FW/1
  28. → I knew I was going to add interceptor support

    next. This blog post made me rethink the implementation from being ColdBox only to something more inclusive. → FW/1 integration is part of the qb docs. → Basic ModuleConfig.cfc support is now in FW/1