Abstract RESTful ORM & Service Factory with Taffy By: Greg Moser

Last week I speant a good portion of my days thinking about the best approach for the RESTful API in Slatwall.  As of right now the primary usage of the API is for Ajax requests, and all of the functions really fall into two categories.

1) Action API: Perform some action like "Add To Cart" or "Update Shipping Method Options".
2) Entity API: Get a list of entities, get a single entity, update an entity, delete entities, ect.

I wrapped up friday afternoon with a single CFC that is 200 lines, it handles any action you could ever want to perform, and maps proper RESTful URI's to every entity in our database.  This is all made possible by making the entire API convention based.

When I approached the 1st requirement months ago, I automatically did what I think most people do and I tried to figure out a way to proxy my exisiting controllers in some fasion or another.  The problem with this approach is that you never get exactly what you want as far as a response goes, and also there tends to be other logic that doesn't pretain to the Ajax call you are executing.

That is when it dawned on me.... "Javascript Is A Controller".  I mean duh!

All of those theoretical reasons for keeping your controller as small as possible make sense now.  If you think about it and ask yourself "What is my controller supposed to do?" Well, it is designed to take an event of some sort - typically a page request - then translate some of the data that came along with that event (think form and url scope), and pass it to the service layer.  The results of that service layer we may format slighly so that our view likes it, and then we are done.

That concept of a controller is exactly what we do with Javascript w/ajax.  The only difference is that the event might be a mouse click or a changed form field.  The data that comes along with that event is pulled out of current page in some fasion or another. All of that information is passed to the service layer but this time via ajax, and then the results are updated slighly to be pushed back into the view.

Now I'm not sure if 99% of the programmers out there already understood this about javascript being your controller, but for me it was a huge awakening.  Mostly what it enable me to realize was "Oh, I just need to expose my entire Service Layer to secure ajax requests."  I already have security in place for handling Ajax request through Taffy, so why not.

Then there is the second part of the overall requirement and that is the Entity API.  We have over 80 entities right now for Slatwall and that list seems to grow every day.  Now I don't know about you, but the whole reason I love ORM is so that I don't need to write the same code over and over again for exach and every object in my model.  The thought of creating a resource for every entity seemed like a nightmare.

Enough with the backstory, lets dig in to how it's setup.  Now I am going to simplify the example code so that it makes more sense outside the context of Slatwall.  First things first we create our AbstractResource.cfc and place it in our /resources folder for taffy to use.

Now to get this to work we need to setup the base resource identifier as a wildcard.  Lets pretend that we were setting up an Order resource and a Product resource.  Well in taffy we would probably create two CFC's and set the URI's to look something like this:

/order/{orderID}/
/product/{productID}/

Instead of doing that we setup our AbstractResource.cfc to use a wildcard URI that looks like this:

/{entityName}/{entityID}/

Taffy maps anything that is inside {} to arguments that get passed to your get, post, put & delete methods inside your component.  With that in mind if you are using ORM, it's as simple as doing this:

 

public any function get(required string entityName, required string entityID) {
  var entity = entityLoadByPK(arguments.entityName, arguments.entityID);
  return representationOf(entity).withStatus(200);
}

public any function put(required string entityName, required string entityID) {
  var entity = entityLoadByPK(arguments.entityName, arguments.entityID);
  // Populate the entity with the data from the request.
  entity.populate(arguments);
  entitySave(entity);
}

public any function post(required string entityName, required string entityID){
  var entity = entityNew(arguments.entityName);
  // Populate the entity with the data from the request
  entity.populate(arguments);
  entitySave(entity);
}

public any function delete(required string entityName, required string entityID) {
  var entity = entityLoadByPK(arguments.entityName, arguments.entityID);
  entityDelete(entity);
}

Now I have really simplified this into something that is almost non-functional but it is just to illustrate a point.  Obvioulsy you would want to validate your data, pass back some responses, ect... but you should get the idea.

This takes care of about 90% of what I want out of an Entity API, but I also want to be able to get lists.  Thats when I realized I could always just pass "list" as the entityID (because we'll never have an ID of 'list').  Then I just look for "list" in my get() method, and I'm good to go.

public any function get(requires string entityName, required string entityID) {
  if(arguments.entityID == "list") {
    var results = entityLoad(entityName);
  } else {
    var results = entityLoadByPK(entityName, entityID)
  }
  return representationOf(results).withStatus(200);
}

In the actual Slatwall implimentation you can ask for a "list" or a "smartList" to allow for paging, sorting, filtering, search, ect.

So this is going pretty well, we have a single CFC with about 40 lines of code, and we can access any object in our database with propper RESTful URI's.  But remember that the other goal of this little project was also the Action API, or as we now know it our Service Layer API (remember no need for a controller).

I imagine that anyone reading this is already using a bean factory for their service layer, and hopefully they have a naming convention like we do.  Bascially all of our services are named with "service" as the suffix, so "productService" or "orderService".  For this Service API, I choose to expose it as a post method, and not on get, put or delete... you can do whatever makes sense for your project.  So how do we impliment it?

First we update our wildcard URI definition:

/{entityNameOrServiceName}/{entityIDorServiceMethod}/

Then we make a small tweek to our post method:

public any function post(required string entityNameOrServiceName, required string entityIDorServiceMethod) {
  if(right(arguments.entityNameOrServiceName, 7) == "service") {
    var service = getBeanFactory().getBean(arguments.entityNameOrServiceName);
    var results = evaluate("service.#arguments.entityIDorServiceMethod#(arguments)");
    return representationOf(results).withStatus(200);
  }
  
  //Continue with standard post logic here...
  // ...
}

That is pretty much it, and now you can use Javascript as your controller to post requests to your new Service API over URI's that look like this:

/orderService/addOrderItem/

If this has at all peeked your interest, then I recommend looking at the GenericAbstractResource.cfc in Slatwall to see the actual logic behind how its done.  Also you may want to look at our Application.cfc because I did make some small tweaks to the security methodology talked about in this post, so that it will work with this new Abstract Resource.

One last bonus feature that I'll mention, is the fact that if at any point for a given resource or entity you want to break out of this abstract role and do custom logic you can.  For instance if you wanted to change the logic around for the product entity - no problem - just add a new resource called Product.cfc, and set the URI as:

/Product/{productID}/

Then extend the AbstractResource.cfc so that you have all of the built in logic you've already created... and then just override whichever method(s) you see fit.  You can almost think of the AbstactResource.cfc as being like onMissingMethod() or in this case onMissingResource.

Comments

Adam Tuttle Posted: October 2, 2011, 11:01 AM

Pretty cool stuff, thanks for sharing.

You actually don't have to use evaluate() to invoke a dynamic function name via script.

You could replace:

evaluate("service.#arguments.entityIDorServiceMethod#(arguments)");

With:

var proxy = service[arguments.entityIDorServiceMethod];

proxy(arguments);

Greg Moser Posted: October 2, 2011, 11:21 AM

@Adam,

That is a really good point and for anyone reading this that should work 99% of the time. I originally had something similar to that, but then ran into issue with our services becuase a lot of the methods we use actually rely on the onMissingMethod() function in our base service.

However now that I just wrote that I can probably just do:

if(structKeyExists(service, arguments.entityIDorServiceMethod)){

var proxy = service[arguments.entityIDorServiceMethod];

} else {

var proxy = service["onMissingMethod"];

}

I'll have to give that a shot... thanks for the great input! And thanks again for Taffy (its awesome!)

Tony Nelson Posted: October 2, 2011, 2:41 PM

You'll want to be careful when using method pointers because the methods aren't evaluated in the same context as when you use evaluate().

When using a method pointer, its variables scope references the current object's variables scope, not the variables scope of the component where the method is defined.

Greg Moser Posted: October 2, 2011, 5:47 PM

@Tony,

That is good to know about the method pointers, I had no idea, and I can imagine that would be a debugging nightmare some random day 3 months from now.

When I was originally doing, I actually creating a method in my base service object called invokeMethod(methodname, methodarguments). That function was designed to execute methods within it's own context. I imagine that if you do it that way you wouldn't run into the same variables scope conflicts.

I guess creating an invokeMethod() that could call anything on itself would defeet the purpose of having public vs private functions, but at the end of the day I'm not sure how big of a deal that is. I also don't know how big of a deal it is to just use evaluate()

Post a Comment
  1. Leave this field empty

Required Field