CPMs: Custom Process Management

The Problematic Workflow

Custom Process Models are perhaps some of the most useful features released for developers since custom objects in the Nov '10 release. They allow custom PHP code to be triggered whenever custom and select standard objects in the system are either created, updated or destroyed. This functionality was fully released in the Nov '12 version.

This is liberating! We now have the ability to run server 'events' on both standard and custom objects. But when you start working with the Process Designer (under Site Configuration in the console) you will notice that the UI and deployment process is extremely clunky. Here's how the Sisyphean CPM development cycle works using the console Process Designer:

Creating a New CPM

  1. Click the New button in the ribbon
  2. Name your CPM
  3. Write your code and tests (really the only important part of the whole process, right?)
  4. Upload your PHP file via the Process Designer UI (only 1 file allowed, mind you!)
  5. Click the save button in the top left
  6. Click the test button in the middle of the screen
  7. Click the OK button after your test methods execute successfully (if they don't, fix your code and go back to the "Upload your PHP file..." step)
  8. Click the Yes button to ensure that you are absolutely certain that your tests executed correctly... are you sure? really?
  9. Drill into the objects in the left pane, find the object you want to attach your process to, and select it
  10. In the right pane, select your process from the menus for the object actions which are relevant
  11. Click the save button in the top left again
  12. Finally, click the deploy button

"Phew! Oh, wait, I forgot something..." Update your code and start back at the "Upload your PHP file..." step. Yes, clunky. If you have very simple logic, this is not an issue, just more of an annoyance, but most of us will want to have a more efficient development process. No matter what, you will have to do this manual process at least once, but this article describes how to setup a standard CP development cycle to upload code via WebDAV that is directly run in your CPM.

The Better (still not perfect) Workflow

If you don't have any experience with CPMs, I'd take a look at the CPMs 101 post before digging into this. What we're going to do is to leverage the Customer Portal WebDAV and deployment system to house our PHP code, which will be called by the CPM framework.

Here's a skeleton incident CPM that does absolutely nothing. We'll start with this and add functionality to it:

<?php /** * CPMObjectEventHandler: Incident * Package: RN * Objects: Incident * Actions: Create, Update, Destroy * Version: 1.2 */ use \RightNow\CPM\v1 as RNCPM; class Incident implements RNCPM\ObjectEventHandler { public static function apply($runMode, $action, $incident, $cycles) { } } class Incident_TestHarness implements RNCPM\ObjectEventHandler_TestHarness { public static function setup() { } public static function fetchObject($action, $objectType) { } public static function validate($action, $object) { return true; } public static function cleanup() { } }

You'll notice the CPM class name is completely generic. I do this for a few reasons:

  1. I like to build a flexible structure that can route logic based on the action (create, update, destroy) within code, and is not reliant on the settings in the CPM UI. I apply the same CPM handler in the UI for object create, update and destroy.
  2. Only a single CPM can be attached to an object type. To add new business logic you need to modify the existing CPM handler, so keep the naming conventions generic from the very beginning.

Then we're going to upload a skeleton PHP class via WebDAV, to the CP libraries folder. In CP3, this is under cp/customer/development/libraries. I typically create cpm/v1 sub-directories to organize my CPM handlers with a version number to make updates easier and less susceptible to side-effects. This file will contain our real CPM business logic:

<?php /** * Skeleton incident cpm handler. */ namespace Custom\Libraries\CPM\v1; class IncidentHandler { static function HandleIncident($runMode, $action, $incident, $cycle) { printf("Testing:\nRun mode: %s\nAction: %s\nIncident ID: %d\nCycle: %d\n\n", Labels::RunMode($runMode), Labels::Action($action), $incident->ID, $cycle); } }

Next we need to modify our CPM to include this library, and use a couple of constants to handle switching between CP development and production mode (if you wanted to get fancy, you could also add staging mode to this, but I find dev and prod are sufficient). Add this after the 'use' statement:

use \Custom\Libraries\CPM\v1 as CPMHandler; /** * When set to 'true', the CPM will run the CP handler library in development * mode. This should be set to 'false' once the handler logic has been tested * and the CP library has been deployed. */ const DEV_MODE = true; define('APPPATH', DEV_MODE ? __DIR__ . "/scripts/cp/customer/development/" : __DIR__ . "/scripts/cp/generated/production/optimized/"); require_once APPPATH . "libraries/cpm/v1/IncidentHandler.php";

And add this to the apply() method, to invoke our library handler:

CPMHandler\IncidentHandler::HandleIncident($runMode, $action, $incident, $cycles);

At this point your development CP library method HandleIncident() will be run anytime an incident is created, updated, or destroyed! Updating the CPM logic is simply a WebDAV push away. Once your logic has been tested, you simply need to deploy your library in CP, and set the DEV_MODE constant to 'false' and your CPM will run the production optimized version of the library.

I've attached a zip of these file skeletons to get you started, with some additional functionality:

  • An example class that appends a string to the subject of an incident on Create or Update
  • A Labels helper class for code-complete and string mapping with the run mode and action CPM constants for easy debugging
Incident Example CPM

Comments

Hey Ben,

nice setup! However I would say that with the new addition of calling CPM's directly from business rules this setup would not be the best choice, as it does not allow to pick specific actions from the business rules..?

Bastiaan

Hey Bastiaan, thanks for pointing this out. When I have a chance to work more intimately with the new business rule functionality, I'll be sure to update this post. In the meantime, any suggestions are welcome!

Great idea for offloading a lot of the work to a library file, however I'm running into some trouble when using my goto debugging of logMessage. I can't seem to get this function to expose to the library. Ive tried doing use Rightnow\Utils\Framework and calling it via Framework::logMessage, but its telling me logMessage not found. Am I running into some sort of namespacing issue in the library? I haven't used this part of the CI framework too much, so I'm not totally sure how to include external libraries.

Placing the library file within the customer portal directory structure simply provides the ability to update via WebDAV and leverage CP's deployment functionality. Customer Portal, however, is not bootstrapped, so none of the standard CP libraries are available for use.

I'd recommend logging to a CBO object, instead, or even a flat-file. (It is possible to manually include some CP utility functionality, but you'd have to resolve any dependencies on the core framework, which would cause a lot of unnecessary overhead.)

I have written a CPM script that runs on Incident update and checks for the value of a custom field. If the value is 1, it updates the incident. Everything works fine except the fact that whenever I update and save the incident, the incident thread gets updated 5 times.

I have added the below line of code already.

if ($cycle !== 0) return;

But, this does not help. I have also noticed that, if at the end of the code that updates the incident, I make the custom field value as 0, it does not update the incident multiple times. But, this does not looks logical and moreover, I do not want to make the value of the custom field again to zero.

Can anyone please suggest some solution here.

Please see my response and question on your discussion post.

For those of you still on CP2, you'll need to set a different reference path for your APPPATH constant

Use this instead:


define('APPPATH',
        DEV_MODE ?
                __DIR__ . "/scripts/euf/application/development/source/" :
                __DIR__ . "/scripts/euf/application/production/optimized/");
                

How to call CP controller function from CPM?

EX: www.domainaname.com/cc/controllername/ref/1231212

How to call above page from CPM, i'm using curl or file_get_content, it showing error.

$curl_result = json_decode(curl_get_contents($url)),true);

Thanks

I do not recommend using curl in this case; the only time curl should be used is when you need to call a web service in a separate system. Encapsulate your controller logic in a CP library class, then include this library file from within your CPM and run it directly.

Dear Guys,

We are trying to creating and updating custom object based on incident status, but custom object not creating. but fetch/update is working fine.

$emp_query = RNCPHP\ROQL::queryObject("SELECT CO.Employee FROM CO.Employee where Incident=$i_id")->next()->next();

// Check if emp already exist or not
if($emp_query==null) $Employee_Object = new RNCPHP\CO\Employee();
else
$Employee_Object = RNCPHP\CO\Employee::fetch($emp_query->ID);

Please can you help me.

Thanks

Hi Ben,

I'm trying read file attachments of incident. The getAdminURL() function is working only in CP files and I'm trying to call controller from CPM(under library) to get the URL link.
I tried using CURL but the its giving 404 error.

Please help to solve this issue

Hi, I am trying to get the RNCPM script to work. There are no errors generated when I test it in Process Designer however Child incidents remain open after I close the associated Parent Incident. Is there something I need to modify in the code in order to get this to work?
<?

/**
Declare the name of this Object Event Handler:
* CPMObjectEventHandler: parentchildupdate

Declare the package this Object Event Handler belongs to:
* Package: OracleServiceCloud

Declare the objects this Object Event Handler can handle:
* Objects: Incident

Declare the actions this Object Event Handler can handle:
* Actions: Update

Declare the Connect Common Object Model version this
Object Event Handler is bound to:
* Version: 1.2
*/

// An alias to use for the version of the Custom Process Model
// this script is binding to:
use \RightNow\CPM\v1 as RNCPM;

// An alias use for the version of Connect for PHP that
// this script is binding to:
use \RightNow\Connect\v1_2 as RNCPHP;

/**
* This class contains the implementation of the Object Event Handler.
* It must be the same name as declared above in the
* CPMObjectEventHandler field in the header.
*/
class parentchildupdate implements RNCPM\ObjectEventHandler
{
/**
* The apply() method "applies" the effects of this handler
* for the given run_mode, action, object and cycle depth.
* Upon a successful return (no errors, uncaught exceptions, etc),
* a "commit" will be implicitly applied.
* @param[in] $run_mode may be one of:
* RNCPM\RunMode{Live,TestObject,TestHarness}
*
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in][out] $object is the Connect for PHP object that was
* acted upon. If $action is Update or Destroy,
* then $object->prev *may* have the previous
* values/state of the object before $action was
* applied.
*
* @param[in] $n_cycles is the number of cycles encountered
* so far during this instance of $action upon
* $object.
*/
public static
function apply( $run_mode, $action, $object, $n_cycles )
{

if ( $object::getMetadata()->COM_type != 'Incident' && $action != "RNCPM\ActionUpdate")
return;
else
{
if($object->StatusWithType->Status->LookupName=='Solved')
{
#If Incident->Status is "Solved", retrieve all the Child Incidents
$roql_result_set = RNCPHP\ROQL::query("SELECT Incident.ID as ID, Incident.PrimaryContact.ParentContact.ID CID FROM Incident WHERE Incident.CustomFields.CO.ParentIncident.ID = ".$object->ID);

$parent_inc = RNCPHP\Incident::fetch($object->ID);

$parent_t_count = count($parent_inc->Threads);

while($roql_result = $roql_result_set->next())
{
while ($row = $roql_result->next())
{
$inc_tmp = RNCPHP\Incident::fetch($row[ID]);
$contact = RNCPHP\Contact::fetch($row[CID]);

$f_count = count($inc_tmp->Threads);
if($f_count == 0)
$inc_tmp->Threads= new RNCPHP\ThreadArray();

#Update the child Incident Thread

$inc_tmp->Threads[$f_count] = new RNCPHP\Thread();
$inc_tmp->Threads[$f_count]->EntryType = new RNCPHP\NamedIDOptList();
$inc_tmp->Threads[$f_count]->EntryType->LookupName = 'Staff Account';

#Check if the recent thread entry is from the agent. If yes, add the entry in the Child Incident's thread
if($parent_t_count > 0 && $parent_inc->Threads[0]->EntryType->LookupName == 'Staff Account' && $parent_inc->Threads[0]->CreatedTime >= $parent_inc->UpdatedTime)
{
$inc_tmp->Threads[$f_count]->Text = strip_tags($parent_inc->Threads[0]->Text);
}
#Else set it to default text.
else
{
$inc_tmp->Threads[$f_count]->Text = "Closing this incident since Parent Incident Status is set to Solved";
}

#Update the Status of the Child Incident
if($inc_tmp->StatusWithType->Status->LookupName != 'Solved')
{
$inc_tmp->StatusWithType->Status = new RNCPHP\NamedIDOptList() ;
$inc_tmp->StatusWithType->Status->LookupName = 'Solved';

$inc_tmp->save();
}
}
}
}
}
$object->save( RNCPHP\RNObject::SuppressAll );
return;
}
}

/**
* This class contains the test harness for the Object Event Handler.
* It must be the same name as declared above but with "_TestHarness"
* added as a suffix on the name.
*/
class parentchildupdate_TestHarness implements RNCPM\ObjectEventHandler_TestHarness
{

/**
* setup() gives one a chance to do any kind of setup necessary
* for the test. The implementation can be empty, but it must exist.
*/
public static function setup()
{
return;
}

/**
* fetchObject() is invoked by the test harness to get the set
* of objects to test with for the given action and object type.
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in] $object_type is the PHP class name of the
* Connect object type being tested.
*
* \returns the object or an array of objects to test with.
*/
public static function fetchObject( $action, $object_type)
{
static $test_objs = array();

// For this test, we'll invent what we need.
if ( isset( $test_objs[$object_type] ) )
{
$obj = $test_objs[$object_type];
} else
{
$obj = new $object_type;

$md = $object_type::getMetadata();

switch ( $md->COM_type )
{
case 'Incident':
$obj->Subject = 'Test Incident Subject';
$obj->PrimaryContact = RNCPHP\Contact::fetch(3);
break;
default:
throw new \Exception( "Unexpected type: $object_type" );
}

switch ( $action )
{
case RNCPM\ActionUpdate:
$verb = 'update';
break;
}
$test_objs[$object_type] = $obj;
echo( "Invented " . $object_type::getMetadata()->COM_type . " for $verb\n" );

// Legacy external events, rules and Custom Processes are
// automatically disabled while in test harness mode.
// So, no need to specify SuppressAll here.
$obj->save();
}

return( $obj );
}

/**
* validate() is invoked by the test harness to validate the
* expected effects of the given action upon the given object.
* Throw an exception or return false to indicate failure.
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in] $object is the Connect for PHP object that was
* acted upon.
*
* \returns true if the test for $action, $object succeeded, or
* false otherwise.
* Throwing an exception is another way of communicating
* failure, and will display the exception text as a
* result of the test.
*/
public static function validate( $action, $object )
{
return true;
}

/**
* cleanup() gives one a chance to do any kind of post-test clean up
* that may be necessary.
* The implementation can be empty, but it must exist.
* Note that the test harness is integrated with the Connect API such
* that operations performed thru the Connect API are not committed,
* so there's no need to clean up after the test even if it has created,
* modified or destroyed objects via the Connect API.
*/
public static function cleanup()
{
return;
}

}

At first glance in your apply() method's first conditional, try $action != RNCPM\ActionUpdate without the double-quotes.

Thanks for the quick response Ben. I did remove the double quotes and tried it again. Unfortunately it didn't seem to make any difference? :-/

Your header might also be causing issues. The CPM system pre-parses the actual multi-line comment header of the file to retrieve some config data. Try this: /** * CPMObjectEventHandler: parentchildupdate * Package: RN * Objects: Incident * Actions: Update * Version: 1.2 */ If that still doesn't work, strip out all of your logic from the CPM and simply echo out some text. Anything you echo should pop up in a dialog box when running the Test in the CPM Designer. If you're in a recent version with access to the BUI, you can also use the New CPM Logging Tools to help debug and trace the logic when not using the Test button. Once you know the CPM is actually running, start adding your business logic back in.

I should note that the test running of the script works fine and there were no errors in the error log. Is there a way to specify run mode Live vs. run mode test TestHarness ? Moreover this script was written by developers at oracle so I am not sure why it's not firing when I open a new Parent incident then open a child incident , then set the status of the parent incident to Solved the child incident remains status New. The only thing I can think of is that maybe the script wasn't tested in OSvC 18A or the run mode needs to be edited?

I'd submit an SR to Oracle. I believe it's still their policy to freely upgrade custom code that their OCS team wrote. Otherwise [shameless plug alert], my team and I are happy to dig deeper and assist! 45 North Solutions

Cool thanks much Ben!

Zircon - This is a contributing Drupal Theme
Design by WeebPal.