Custom Validation with Generic Commit: a Model-Glue Case Study
Someone recently asked about how to specifically ensure that a submitted email address is unique when using a model-glue generic commit, so I thought I'd share an example since I recently had to do that very thing.
I'm assuming for the remainder of this post that the reader is already familiar with Model-Glue in a practical sense, and at least knows of the existence of Reactor's automagic validation. Still, I'll try not to leave out too many relevant details.
Okay, the scenario:
I have a secured app, and I want to give new users the opportunity to sign up for an account. I'm using email address as the user name since in theory it should always be unique to an individual. (note: I opted NOT to set up my user table so that the email address field has a unique index on it) So, the user clicks "Sign me up!", I present them with a form to fill out, one field being their email address. They submit the form, and here's where we dive down under the waves to see what's happening...
The form submits to the event "inspector.create", which in the modelglue.xml file reads as follows:
Notice we're using a generic commit to handle this, which works because all of the needed data resides within form fields that are named exactly as their database field counterparts (eg; in my table there's a field called 'email', in my form there's a form field named 'email', etc.).
Now, the fact that we have specified an argument named "validationName" in our generic commit means that before the form data is committed to the database, Reactor is going to invoke the aid of one of the CFCs it auto-generated for us in order to "validate" the info submitted. By default, validation consists of checks that were created based on your table's metadata (unique indexes, datatypes, null not allowed, etc.), but Reactor was kind enough to provide us a convenient place to extend and customize that default validation if we so desire. In my scenario, since I did NOT choose to put a rule in place specifying that my email field should be unique, I added a custom method to perform that check.
To locate the CFC for adding custom validation, look in \model\data\reactor\Validator\, and find the cfc named after your target table. In my case, it's UserValidator.cfc
By default, the guts of the Validator CFCs you'll be working with look similar to the following:
I decided to add two more methods of validation to my user object:
The method present here that you may not have anticipated is called "validate", and is the exact same name as the method you would find in Reactor's core user validation object. So, what have we in effect done, boys and girls? That's right! We have (choose your favorite word, they both mean the same thing) overloaded/overriden the main "validate" method, in order to ensure that not only the original, auto-generated validation methods get called, but also the two new ones we added after the fact.
Let's take a closer look at our version of the "validate" method (this will only take a second, there are a couple of important things to note in there).
First off, you'll note that every validation method requires two arguments: an incoming record whose values are being validated, and the errorcollection where we (dang, this makes too much sense!) collect our errors.
Second, notice that we are FIRST executing our custom validation methods, then afterwards executing the auto-generated "validate" method by calling the object we extended, directly, via a call to "SUPER". Very cool, eh? Even though we initially overloaded our validate method in order to ensure that it got called rather than the core version of it, we were STILL able to call the original version as well. (By the way, that is a little trick I learned from Doug Sims' blog www.evenamonkey.com).
Alright then, we have submitted our form, used generic commit to perform validation, that validation called our extended object, executed the local custom methods first, then the system validate method. If any errors were encountered, our generic commit would have added a result named "ValidationError" to the event bucket (see the modelglue.xml snippet above), thus redirecting us back to the original page (where we have code in place looking for the presence of the error collection). If no errors were encountered, we're directed forward to the next event in the chain, and all is well.
One Mo Thang
Ah, one last thing that is of great importance to be aware of regarding Reactor validation: The Dictionary. The dictionary is an xml file that is specific to a validation object. In our example, since we have a userValidator object, there also exists a \model\data\reactor\Dictionary\userdictionary.xml file. This file is used to look up and translate any errors encountered so that the user is presented with readable text rather than a cryptic message. When you add custom validation methods, you also need to add dictionary entries. Consider the following snippet from my userdictionary.xml file:
Look back at the custom method "validateEmail" we added earlier, and notice that if our validation fails, we're adding an error that looks like
<CFSET arguments.ErrorCollection.addError("user.email.alreadyexists") />
The syntax of that message is no coincidence...it's the same syntax you would use to access an item in an XML file. Fancy that! 'user' denotes the user dictionary; 'email' denotes the particular table field; and 'alreadyexists' is a term I just made up, and indicates that Reactor should look for a tag called 'alreadyexists' in order to find the correct translation for this error. Thus, you'll notice the tag
<alreadyexists>That email address is already being used. Please select another email address. If you have forgotten your password, return to the main login screen and select "Forgot Password"</alreadyexists>
in the <email> section of our dictionary file.
To Sum it all up!
Okay, so in a nutshell, if you have a form being submitted and you want to ensure that the email address is unique (AND you haven't put a rule in place within the database itself so specifying this):
That's it!
Doug out.
I'm assuming for the remainder of this post that the reader is already familiar with Model-Glue in a practical sense, and at least knows of the existence of Reactor's automagic validation. Still, I'll try not to leave out too many relevant details.
Okay, the scenario:
I have a secured app, and I want to give new users the opportunity to sign up for an account. I'm using email address as the user name since in theory it should always be unique to an individual. (note: I opted NOT to set up my user table so that the email address field has a unique index on it) So, the user clicks "Sign me up!", I present them with a form to fill out, one field being their email address. They submit the form, and here's where we dive down under the waves to see what's happening...
The form submits to the event "inspector.create", which in the modelglue.xml file reads as follows:
<event-handler name="inspector.create">
<broadcasts>
<message name="ModelGlue.genericCommit">
<argument name="recordName" value="UserRecord" />
<argument name="criteria" value="" />
<argument name="object" value="User" />
<argument name="validationName" value="UserValidation" />
</message>
</broadcasts>
<views></views>
<results>
<result name="commit" do="inspector.newuser" redirect="true" append="email" preserveState="false" />
<result name="validationError" do="inspector.signup" redirect="false" append="" preserveState="true" />
</results>
</event-handler>
<broadcasts>
<message name="ModelGlue.genericCommit">
<argument name="recordName" value="UserRecord" />
<argument name="criteria" value="" />
<argument name="object" value="User" />
<argument name="validationName" value="UserValidation" />
</message>
</broadcasts>
<views></views>
<results>
<result name="commit" do="inspector.newuser" redirect="true" append="email" preserveState="false" />
<result name="validationError" do="inspector.signup" redirect="false" append="" preserveState="true" />
</results>
</event-handler>
Notice we're using a generic commit to handle this, which works because all of the needed data resides within form fields that are named exactly as their database field counterparts (eg; in my table there's a field called 'email', in my form there's a form field named 'email', etc.).
Now, the fact that we have specified an argument named "validationName" in our generic commit means that before the form data is committed to the database, Reactor is going to invoke the aid of one of the CFCs it auto-generated for us in order to "validate" the info submitted. By default, validation consists of checks that were created based on your table's metadata (unique indexes, datatypes, null not allowed, etc.), but Reactor was kind enough to provide us a convenient place to extend and customize that default validation if we so desire. In my scenario, since I did NOT choose to put a rule in place specifying that my email field should be unique, I added a custom method to perform that check.
To locate the CFC for adding custom validation, look in \model\data\reactor\Validator\, and find the cfc named after your target table. In my case, it's UserValidator.cfc
By default, the guts of the Validator CFCs you'll be working with look similar to the following:
<cfcomponent hint="I am the validator object for the Section object. I am generated, but not overwritten if I exist. You are safe to edit me."
extends="reactor.project.myprojectname.Validator.SectionValidator">
<!--- Place custom code here, it will not be overwritten --->
</cfcomponent>
extends="reactor.project.myprojectname.Validator.SectionValidator">
<!--- Place custom code here, it will not be overwritten --->
</cfcomponent>
I decided to add two more methods of validation to my user object:
- make sure they typed their password in twice the same way,
- and make sure their email isn't already used in the system
<cfcomponent hint="I am the validator object for the User object. I am generated, but not overwritten if I exist. You are safe to edit me."
extends="reactor.project.myprojectname.Validator.UserValidator">
<CFFUNCTION name="validate" access="public" hint="I validate an record" output="false" returntype="any" _returntype="reactor.util.ErrorCollection">
<cfargument name="UserRecord" hint="I am the Record to validate." required="no" type="any" _type="reactor.project.housefacks.Record.UserRecord" />
<cfargument name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="any" _type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<CFSET validatePasswordVals(arguments.UserRecord, arguments.ErrorCollection) />
<CFSET validateEmail(arguments.UserRecord, arguments.ErrorCollection) />
<CFSET super.validate(arguments.UserRecord, arguments.ErrorCollection) />
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
<CFFUNCTION name="validatePasswordVals" access="public" output="false" returntype="reactor.util.ErrorCollection">
<CFARGUMENT name="UserRecord" hint="I am the Record to validate." required="no" type="reactor.project.myprojectname.Record.UserRecord" />
<CFARGUMENT name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<!--Blue is only allowed as a selection for Sky for people whose weathercode = 5-->
<CFIF arguments.UserRecord.getPassword() is not arguments.UserRecord.getConfirmPassword() >
<CFSET arguments.ErrorCollection.addError("user.password.notconfirmed") />
</CFIF>
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
<CFFUNCTION name="validateEmail" access="public" output="false" returntype="reactor.util.ErrorCollection">
<CFARGUMENT name="UserRecord" hint="I am the Record to validate." required="no" type="reactor.project.myprojectname.Record.UserRecord" />
<CFARGUMENT name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<cfset var userGateway = "" />
<CFIF arguments.UserRecord.getEmail() is not "" >
<cfset userGateway = reactorfactory.createGateway("user").getByFields(email=arguments.UserRecord.getEmail()) />
<cfif userGateway.recordcount IS NOT 0>
<CFSET arguments.ErrorCollection.addError("user.email.alreadyexists") />
</cfif>
</CFIF>
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
</cfcomponent>
extends="reactor.project.myprojectname.Validator.UserValidator">
<CFFUNCTION name="validate" access="public" hint="I validate an record" output="false" returntype="any" _returntype="reactor.util.ErrorCollection">
<cfargument name="UserRecord" hint="I am the Record to validate." required="no" type="any" _type="reactor.project.housefacks.Record.UserRecord" />
<cfargument name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="any" _type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<CFSET validatePasswordVals(arguments.UserRecord, arguments.ErrorCollection) />
<CFSET validateEmail(arguments.UserRecord, arguments.ErrorCollection) />
<CFSET super.validate(arguments.UserRecord, arguments.ErrorCollection) />
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
<CFFUNCTION name="validatePasswordVals" access="public" output="false" returntype="reactor.util.ErrorCollection">
<CFARGUMENT name="UserRecord" hint="I am the Record to validate." required="no" type="reactor.project.myprojectname.Record.UserRecord" />
<CFARGUMENT name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<!--Blue is only allowed as a selection for Sky for people whose weathercode = 5-->
<CFIF arguments.UserRecord.getPassword() is not arguments.UserRecord.getConfirmPassword() >
<CFSET arguments.ErrorCollection.addError("user.password.notconfirmed") />
</CFIF>
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
<CFFUNCTION name="validateEmail" access="public" output="false" returntype="reactor.util.ErrorCollection">
<CFARGUMENT name="UserRecord" hint="I am the Record to validate." required="no" type="reactor.project.myprojectname.Record.UserRecord" />
<CFARGUMENT name="ErrorCollection" hint="I am the error collection to populate. If not provided a new collection is created." required="no" type="reactor.util.ErrorCollection" default="#createErrorCollection(arguments.UserRecord._getDictionary())#" />
<cfset var userGateway = "" />
<CFIF arguments.UserRecord.getEmail() is not "" >
<cfset userGateway = reactorfactory.createGateway("user").getByFields(email=arguments.UserRecord.getEmail()) />
<cfif userGateway.recordcount IS NOT 0>
<CFSET arguments.ErrorCollection.addError("user.email.alreadyexists") />
</cfif>
</CFIF>
<CFRETURN arguments.ErrorCollection />
</CFFUNCTION>
</cfcomponent>
The method present here that you may not have anticipated is called "validate", and is the exact same name as the method you would find in Reactor's core user validation object. So, what have we in effect done, boys and girls? That's right! We have (choose your favorite word, they both mean the same thing) overloaded/overriden the main "validate" method, in order to ensure that not only the original, auto-generated validation methods get called, but also the two new ones we added after the fact.
Let's take a closer look at our version of the "validate" method (this will only take a second, there are a couple of important things to note in there).
First off, you'll note that every validation method requires two arguments: an incoming record whose values are being validated, and the errorcollection where we (dang, this makes too much sense!) collect our errors.
Second, notice that we are FIRST executing our custom validation methods, then afterwards executing the auto-generated "validate" method by calling the object we extended, directly, via a call to "SUPER". Very cool, eh? Even though we initially overloaded our validate method in order to ensure that it got called rather than the core version of it, we were STILL able to call the original version as well. (By the way, that is a little trick I learned from Doug Sims' blog www.evenamonkey.com).
Alright then, we have submitted our form, used generic commit to perform validation, that validation called our extended object, executed the local custom methods first, then the system validate method. If any errors were encountered, our generic commit would have added a result named "ValidationError" to the event bucket (see the modelglue.xml snippet above), thus redirecting us back to the original page (where we have code in place looking for the presence of the error collection). If no errors were encountered, we're directed forward to the next event in the chain, and all is well.
One Mo Thang
Ah, one last thing that is of great importance to be aware of regarding Reactor validation: The Dictionary. The dictionary is an xml file that is specific to a validation object. In our example, since we have a userValidator object, there also exists a \model\data\reactor\Dictionary\userdictionary.xml file. This file is used to look up and translate any errors encountered so that the user is presented with readable text rather than a cryptic message. When you add custom validation methods, you also need to add dictionary entries. Consider the following snippet from my userdictionary.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<User>
<email>
<label>email</label>
<comment/>
<maxlength>100</maxlength>
<scale>0</scale>
<invalidType>The email field does not contain valid data. This field must be a string value.</invalidType>
<invalidLength>The email field is too long. This field must be no more than 100 bytes long.</invalidLength>
<notProvided>The email field is required but was not provided.</notProvided>
<alreadyexists>That email address is already being used. Please select another email address. If you have forgotten your password, return to the main login screen and select "Forgot Password"</alreadyexists>
</email>
<password>
<label>password</label>
<comment/>
<maxlength>50</maxlength>
<scale>0</scale>
<invalidType>The password field does not contain valid data. This field must be a string value.</invalidType>
<invalidLength>The password field is too long. This field must be no more than 50 bytes long.</invalidLength>
<notProvided>The password field is required but was not provided.</notProvided>
<notconfirmed>The password you typed in is not the same as the "confirm password" value.</notconfirmed>
</password>
</User>
<User>
<email>
<label>email</label>
<comment/>
<maxlength>100</maxlength>
<scale>0</scale>
<invalidType>The email field does not contain valid data. This field must be a string value.</invalidType>
<invalidLength>The email field is too long. This field must be no more than 100 bytes long.</invalidLength>
<notProvided>The email field is required but was not provided.</notProvided>
<alreadyexists>That email address is already being used. Please select another email address. If you have forgotten your password, return to the main login screen and select "Forgot Password"</alreadyexists>
</email>
<password>
<label>password</label>
<comment/>
<maxlength>50</maxlength>
<scale>0</scale>
<invalidType>The password field does not contain valid data. This field must be a string value.</invalidType>
<invalidLength>The password field is too long. This field must be no more than 50 bytes long.</invalidLength>
<notProvided>The password field is required but was not provided.</notProvided>
<notconfirmed>The password you typed in is not the same as the "confirm password" value.</notconfirmed>
</password>
</User>
Look back at the custom method "validateEmail" we added earlier, and notice that if our validation fails, we're adding an error that looks like
<CFSET arguments.ErrorCollection.addError("user.email.alreadyexists") />
The syntax of that message is no coincidence...it's the same syntax you would use to access an item in an XML file. Fancy that! 'user' denotes the user dictionary; 'email' denotes the particular table field; and 'alreadyexists' is a term I just made up, and indicates that Reactor should look for a tag called 'alreadyexists' in order to find the correct translation for this error. Thus, you'll notice the tag
<alreadyexists>That email address is already being used. Please select another email address. If you have forgotten your password, return to the main login screen and select "Forgot Password"</alreadyexists>
To Sum it all up!
Okay, so in a nutshell, if you have a form being submitted and you want to ensure that the email address is unique (AND you haven't put a rule in place within the database itself so specifying this):
- add a method to your customizable validator CFC for the target table;
- add a 'validate' method to the same CFC in order to overload the system version of the same;
- within your custom 'validate' method, execute your custom method first
- within the same, execute the system version of validate using "SUPER.Validate()"
- edit your dictionary file to add a translation for your new custom error
That's it!
Doug out.
