Skip to content

Instantly share code, notes, and snippets.

@elpete
Last active October 5, 2015 15:13
Show Gist options
  • Save elpete/f22fc8160101c79c7500 to your computer and use it in GitHub Desktop.
Save elpete/f22fc8160101c79c7500 to your computer and use it in GitHub Desktop.
ColdFusion Model Relationships without an ORM

This is a proof-of-concept for relationships without using an ORM in ColdFusion (specifically, ColdBox — you can replace "wirebox" with your favorite DI framework).

The API I'm trying to get is borrowed from Laravel. We are not using an ORM, though, and have traditionally handled the requests using a {Object}Service -> {Object}DAO -> {Object} pattern. This is my attempt to create the nice relationship syntax found in Laravel for our non-ORM stack.

Things I want to keep:

  • the great syntax: user.roles()
  • the fact that the related objects are not created unless called
  • not having to call a different method or pass a different parameter to the service to compose the relationships
    • (though, if you have a strong opinion about that, I'm interested in at least hearing why)

I'd love comments on this. I'm trying to learn the best way to accomplish this given our stack.

Thanks!

component name="Permission" {
property name="id";
property name="name";
property name="roles";
public Permission function init() {
return this;
}
public array function getRoles() {
if (!variables.keyExists('roles')) {
variables.roles = variables.roles();
}
return variables.roles;
}
private array function roles() {
return wirebox.getInstance('RoleService')
.findByPermission(this);
}
}
component name="PermissionService" {
property name="dao" inject="PermissionDAO";
property name="wirebox" inject="wirebox";
property name="populator" inject="wirebox:populator";
public PermissionService function init() {
return this;
}
public function findByRole(required Role role) {
return this.findByRoleId(arguments.role.id);
}
public function findByRoleId(
required numeric roleId,
boolean asQuery = false
) {
var q = dao.findByRoleId(arguments.roleId);
if (arguments.asQuery) {
return q;
}
var permissions = [];
for(var x=1; x lte q.recordcount; x++) {
arrayAppend(
permissions,
populator.populateFromQuery(
wirebox.getInstance("Permission"), q, x
)
);
}
return permissions;
}
}
component name="Role" {
property name="id";
property name="name";
public Role function init() {
return this;
}
public array function users() {
return wirebox.getInstance('UserService').findByRole(this);
}
public array function permissions() {
return wirebox.getInstance('PermissionService').findByRole(this);
}
}
component name="RoleService" {
property name="dao" inject="RoleDAO";
property name="wirebox" inject="wirebox";
property name="populator" inject="wirebox:populator";
public RoleService function init() {
return this;
}
public function findByUser(required User user) {
return this.findByUserId(arguments.user.id);
}
public function findByUserId(
required numeric userId,
boolean asQuery = false
) {
var q = dao.findByUserId(arguments.userId);
if (arguments.asQuery) {
return q;
}
var roles = [];
for(var x=1; x lte q.recordcount; x++) {
arrayAppend(
roles,
populator.populateFromQuery(
wirebox.getInstance("Role"), q, x
)
);
}
return roles;
}
public function findByPermission(required Permission permission) {
return this.findByPermissionId(arguments.permission.id);
}
public function findByPermissionId(
required numeric permissionId,
boolean asQuery = false
) {
var q = dao.findByPermissionId(arguments.permissionId);
if (arguments.asQuery) {
return q;
}
var roles = [];
for(var x=1; x lte q.recordcount; x++) {
arrayAppend(
roles,
populator.populateFromQuery(
wirebox.getInstance("Role"), q, x
)
);
}
return roles;
}
}
component name="User" {
property name="id";
property name="name";
public User function init() {
return this;
}
public array function roles() {
return wirebox.getInstance('RoleService').findByUser(this);
}
}
component name="UserService" {
property name="dao" inject="UserDAO";
property name="wirebox" inject="wirebox";
property name="populator" inject="wirebox:populator";
public UserService function init() {
return this;
}
public function findByRole(required Role role) {
return this.findByRoleId(arguments.role.id);
}
public function findByRoleId(
required numeric roleId,
boolean asQuery = false
) {
var q = dao.findByRoleId(arguments.roleId);
if (arguments.asQuery) {
return q;
}
var users = [];
for(var x=1; x lte q.recordcount; x++) {
arrayAppend(
users,
populator.populateFromQuery(
wirebox.getInstance("User"), q, x
)
);
}
return users;
}
}
@elpete
Copy link
Author

elpete commented Oct 5, 2015

So it seems like one big downside of this approach is the lack of caching on models. Looking into Eloquent, it makes a distinction between user.roles() and user.roles. The first returns a scoped query and the second returns the collection. Also, the second will cache the result on the parent model.

@elpete
Copy link
Author

elpete commented Oct 5, 2015

I updated Permission.cfc to show a different approach. This leverages implicit accessors (this.invokeImplicitAccessor = true; in Application.cfc) and caching. Calling permission.roles will first fire off to the RoleService and cache the result for future calls. The query builder idea is interesting, but not really fleshed out in ColdBox that I know of.

@elpete
Copy link
Author

elpete commented Oct 5, 2015

More learning — it seems like implicit accessor calls to get{PropertyName} do not pass through onMissingMethod while explicit calls do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment