Using Metro - Creating Concrete Services and Gateways

2008 December 22
by Paul Marcotte

In my brief introduction to Metro, I provided some cursory configuration details. Today, I'll describe portions of the included sample security package to demonstrate how to integrate Metro and leverage the simple User, Role, Permission model. Since Metro is designed specifically for use with Transfer ORM, we'll need to set up a database, dsn and configure Transfer and ColdSpring for this demo.

I've included sql files for both MySQL and MSSQL within the package to create tables and insert some sample data. Part of the power behind using an ORM is that the data store is interchangeable. In my experience, it is uncommon for a project to swap data stores, but nice to know that you can.

Let's look at the transfer.xml file in the security package. The following describes a User object with manytoone Role which has manytomany Permissions.

<?xml version="1.0" encoding="UTF-8"?>
<transfer xsi:noNamespaceSchemaLocation="http://www.transfer-orm.com/transfer/resources/xsd/transfer.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<InvalidTagDefinitions>
<package name="security">
<object name="User" table="tbl_user" decorator="metro.security.User">
<id name="UserId" type="numeric" />
<property name="Firstname" type="string" />
<property name="Lastname" type="string" />
<property name="Email" type="string" />
<property name="Username" type="string" />
<property name="Password" type="string" />
<manytoone name="Role" lazy="false">
<link to="security.Role" column="RoleId"/>
</manytoone>
</object>
<object name="Role" table="tbl_role">
<id name="RoleId" type="numeric" />
<property name="Name" type="string" />
<property name="Description" type="string" />
<manytomany name="Permission" table="tbl_role_permission" lazy="false" proxied="false">
<link column="RoleId" to="security.Role"/>
<link column="PermissionId" to="security.Permission"/>
<collection type="array">
<order property="Name"/>
</collection>
</manytomany>
</object>
<object name="Permission" table="tbl_permission">
<id name="PermissionId" type="numeric" />
<property name="Name" type="string" />
<property name="Description" type="string" />
</object>
</package>
</objectDefinitions>
</transfer>

As a rapid application development tool, Metro can get you rolling with CRUD and list operations quickly, but applications have far more behaviour than that. This is where we can start leveraging the more powerful features in Metro by creating concrete Services and Gateways that extend the core components. The example SecurityService below contains a few typical methods for user session management.

<cfcomponent displayname="SecurityService" extends="metro.core.Service" output="false">

<cffunction name="getCurrentUser" access="public" output="false" returntype="any">
<cfif userInSession()>
<cfreturn get("User",getUserSession())>
<cfelse>
<cfreturn new("User")>
</cfif>
</cffunction>

<cffunction name="userInSession" access="public" output="false" returntype="boolean">
<cfreturn StructKeyExists(session,"userId")>
</cffunction>

<cffunction name="setUserSession" access="public" output="false" returntype="void">
<cfargument name="UserId" type="numeric" required="true" />
<cfset session.UserId = arguments.UserId>
</cffunction>

<cffunction name="getUserSession" access="public" output="false" returntype="any">
<cfreturn session.UserId />
</cffunction>

<cffunction name="loginUser" access="public" output="false" returntype="any">
<cfset var user = new("User") />
<cfset var result = getTransientFactory().create("Result")>
<cfset var errors = StructNew()>
<cfset result.setErrors(user.populate(argumentCollection=arguments))>
<cfif (result.getSuccess())>
<cfset result.setErrors(user.validate("login"))>
<cfif (result.getSuccess())>
<cfset user = getGateway("User").loginUser(user)>
<cfif user.getIsPersisted()>
<cfset setUserSession(user.getUserId())>
<cfelse>
<cfset errors['badCredentials'] = "Username/Password credentials invalid. Please try again.">
<cfset result.setErrors(errors)>
</cfif>
</cfif>
</cfif>
<cfset result.setResult(user)>
<cfreturn result>
</cffunction>

<cffunction name="logoutUser" access="public" output="false" returntype="boolean">
<cfset var ended = false>
<cfif userInSession()>
<cfset ended = StructDelete(session,"UserId")>
</cfif>
<cfreturn ended>
</cffunction>

<cffunction name="registerUser" access="public" output="false" returntype="any">
<cfset var user = new("User") />
<cfset var result = getTransientFactory().create("Result")>
<!--- populate the bean to test data types --->
<cfset result.setErrors(user.populate(argumentCollection=arguments))>
<cfif (result.getSuccess())>
<!--- validate for register --->
<cfset result.setErrors(user.validate("register"))>
<cfif (result.getSuccess())>
<cfset user.setRole(get("Role",1))>
<!--- save the user --->
<cfset getGateway("User").save(user)>
</cfif>
</cfif>
<cfset result.setResult(user)>
<cfreturn result>
</cffunction>

</cfcomponent>

So how does one define a concrete Service or Gateway with Metro? The details are in the Metro configuration with the two most important variables being the componentPath passed to the ServiceFactory and the packageName used with the getService() factory-method. Here is a snippet from the ColdSpring bean definitions used to configure Metro.

<bean id="ServiceFactory" class="metro.factory.ServiceFactory" lazy-init="false">
<constructor-arg name="TransferFactory">
<ref bean="TransferFactory" />
</constructor-arg>
<constructor-arg name="TransferConfig">
<ref bean="TransferConfig" />
</constructor-arg>
<constructor-arg name="TransientFactory">
<ref bean="TransientFactory" />
</constructor-arg>
<constructor-arg name="componentPath">
<value>model</value>
</constructor-arg>
</bean>

<bean id="SecurityService" factory-bean="ServiceFactory" factory-method="getService">
<constructor-arg name="packageName"><value>security</value></constructor-arg>
</bean>

ComponentPath is a constructor argument that represents the path to the folder in which your packages reside. As a convention, Transfer package names and physical package folders on your file system should match. If you have an accounting package in transfer and you want to use a concrete AccountingService, then you will need an accounting package on the file system with components that extend the metro.core.* components. When you specify the packageName to the createService() factory method, Metro will introspect the components in {componentPath}.{packageName} and if found, instantiate those components instead of the core ones. The Metro convention of one service per transfer package should be mirrored in the package directory, so that only one concrete Service extend metro.core.Service, while many Gateways may extend metro.core.Gateway.

Lastly, if the requested package is not found in the componentPath, the ServiceFactory will look for the package under the Metro library and use any concrete components found there. Therefore, you can implement the security package in Metro by simply running the sql script against your db, creating a dsn, including the transfer object definitions in your transfer xml (see snippet below) and setting up Metro within ColdSpring.

<includes>
<include path="/metro/security/transfer.xml" />
</includes>

Even if you are not an MXUnit user, you can examine the files in the tests/config folder to get a picture of the configuration required to integrate Metro into your (rapid) application development.

In my next post, I'll go over the Metro Decorator and Validator.