Monday, 14 April 2014

Detecting When Values Are Changed

Xataface provides hooks that allow you to respond to many events in the workflow of your application.  The most fundamental hooks involve saving records.  These include:

  • beforeSave - called just before a record is saved to the database.
  • afterSave - called just after a record is saved to the database.
  • beforeInsert - called just before a record is inserted into the database.
  • afterInsert - called just after a record is inserted into the database.
  • beforeUpdate - called just before an existing record is updated in the database.
  • afterUpdate - called just after an existing record is updated in the database.
Notice that before/afterSave is called every time a record is saved (i.e. for both updates and inserts).  One common requirement is to perform an action only if a particular value was changed in the record.  For example, suppose you have a "status" field in your table, and if this field is set to "broken", an email should be sent to the site administrator to "fix" whatever is broken.  A first attempt at solving this problem might be:
This will send an email to the admin (assume that ADMIN_EMAIL is a constant defined elsewhere) every time a record is saved and the 'status' is set to 'broken'.  But there is a major problem with this solution.  This will send an email *every* time the record is saved.  So if the record was set to broken, then a user went in to make additional changes, it would send another email after each save.  Ideally we want to just send the email when the value of 'status' first changed to 'broken'.

Detecting Field Value Changes

Dataface_Record has a valueChanged() method that makes it easy to detect if a field has been modified since the record was last loaded.  With this knowledge, you might try to modify your afterSave() method as follows:
Unfortunately, if you try to run this code, you'll find that the mail is now *never* sent. This is because the "changed" flags on Dataface_Record objects are reset before any of the afterXXX() methods are called. If you want to detect a change, you need to do so in the beforeXXX() methods of the save cycle. E.g.
Now if you try out the application it should work the way that you expect. If the value of the 'status' field is changed, an email will be sent as expected. But this still isn't ideal because the email is technically sent before the "save" cycle is finished. So there is a possibility that a field validator causes the save to be canceled or fail. That may seem like a slim possibility, but it is better to be sure.
So, our ideal solution is to use the afterSave() hook, but to only send the email if the status field has changed.

Passing Data from beforeSave to afterSave Callback

The solution, then, is to use the beforeSave() handler to detect the change, and then somehow tell the afterSave() callback to perform the update in the case that a change is found.  There are many ways to pass data between these methods, but my favourite solution is to use the "pouch" of the Dataface_Record object.
The "pouch" is just an associative array that is a member of a Dataface_Record object.  There you can store arbitrary data to be passed around and between contexts of your application.   In the following example, we use the beforeSave() handler to detect the change in the 'status' field.  We then register a flag in the record's pouch which will be detected in the afterSave() method.
This version works exactly as required. Notice that after detecting the 'send_broken_email' flag in the afterSave() method, it clears this flag so that it doesn't affect future cycles, in case the same record object is saved multiple times during the same HTTP request.

Using PHP Anonymous Functions


The previous version works fine, but as a matter of style, I prefer to use PHP's anonymous functions (available since 5.3) to define callbacks in the beforeSave() handler which can be called in the afterSave() handler.

The thing I like about this style is that all of the business logic will then reside in the beforeSave() method. If I create additional rules that require some action in the afterSave() method, I can just add more callbacks. The afterSave() method doesn't need to change.. it just faithfully executes the list of handlers. This could even be extended to add callbacks from other areas of your app. But I'll leave that for another post.

4 comments:

  1. Interesting... could you use this to create an 'Audit Trail'?
    A simple comparison between the the 'beforeUpdate' and 'afterUpdate' data should enable you to flag any changed data and store it into a log file or table

    ReplyDelete
    Replies
    1. Yes you could implement such a thing using this method. You may also want to check out xataface's history feature as it does something similar. http://xataface.com/documentation/how-to/history-howto

      Delete
  2. Thank you for a really useful blog entry. One gotcha that I fell down that is probably obvious to others but...don't change and save fields in the same record in the afterSave method - endless recursion ensues.....

    ReplyDelete