Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API © RightNow Technologies, Inc. Key Features Object-oriented binding of the RightNow Connect Common Object Model (CCOM) for PHP Uses namespaces for versioning Active-record-like methods on “primary”/crud-able objects (those that derive from RNObject): fetch(), first(), find() save() • Used for both “create” (new/save()) and “update” (fetch()/modify/save()) destroy() “Lazy” Loading fetch(), first(), find() minimally instantiate objects with just the ID. Object properties are not populated/fetched until they are accessed. Object is not fetched from the database until the first non-key field is accessed. “Sub-tables” (e.g. notes, custom fields, file attachments, …) are not fetched from the database until they are accessed. print_r() will only show the content of those properties that have been explicitly set or previously accessed since the last save(). print_r() will show all other properties as having a NULL value. Same ID of the same object type is the same object instance, e.g: Contact::first( “ID = 1” ) === Contact::fetch( 1 ) Objects of the same type with the same id are “shared” throughout the entire process. Key Features (cont’d) Properties that represent foreign keys to other tables/objects are represented as object references of the expected type. Only instantiated when accessed, and then only minimally (just the ID) until a non-key property is accessed. See “Lazy” Loading, above. Custom Fields Objects that have custom fields will have a non-empty CustomFields property on them that is itself an object containing the custom fields as properties. Error handling Errors are thrown as exceptions. Return values are never used to indicate error status. Empty/null results, e.g. a query that doesn’t match anything, will return null. However, a bad query will throw an exception. Constraint tests are performed upon assignment And sometimes upon save(). Some properties of some sub-objects (e.g. Email) have different constraints depending upon the hierarchy they are in, and so can only be tested if the hierarchy is known, or upon save(). Strong object typing Assigning the wrong type to a property will throw an exception. ID and LookupName are read-only on primary objects. ID and LookupName may only be “set” indirectly on primary objects by using fetch(), first(), find() or new/save(). Implicit commit & mail_commit upon successful (0) script exit. Non-zero exit codes will not commit. Getting Started In a CP Model: require_once( get_cfg_var('doc_root') .'/ConnectPHP/Connect_init.php' ); initConnectAPI(); That’s it! More than once is okay: 1st-time ~13ms 2nd time: ~80us So, don’t worry too much about multiple CPHP initializations. Versioned Namespace ConnectPHP uses namespaces to version its interfaces and the presentation of the Connect Common Object Model. The version 1 interface to the classes, constants and static methods of CPHP is in the namespace: RightNow\Connect\v1 Version 1.1 would be in: RightNow\Connect\v1_1 The namespace, or an alias to a namespace must prefix the classname: $a_new_contact = new RightNow\Connect\v1\Contact; OR: use RightNow\Connect\v1 as RNCPHP; $a_new_contact = new RNCPHP\Contact; PHP “use” statements must be in file or namespace scope. They cannot be specified within functions or any other block or scope. The alias defined by the “use” statement must be unique within the file or namespace scope that it is declared. Be careful when used in CP widgets as staging/deployment may put different widgets together in the same file. Hereafter, “RNCPHP” will be used as a namespace alias as declared above. Error Handling Any operation upon or involving any CPHP object, method or property, including accessing or assigning a property may throw an exception. An exception thrown without a corresponding catch will result in a fatal uncaught exception error. Wrap code with try/catch! Wrap blocks of related code. Wrap as large of a scope as you can reasonably handle/report errors to the user. Do let CPHP perform constraint testing for you (it will anyway). • Easier to consume new versions should the constraints change. • Use CPHP metadata to communicate constraints to client code so that constraints can be tested at the client without being hard-wired. ConnectPHP Exceptions derive from the base PHP Exception class. ConnectAPIError for run-of-the-mill errors Use ->getMessage() to get error message. Use ->getCode() to get error code • E.g. RNCPHP\ConnectAPIError::ErrorConstraint ConnectAPIErrorFatal for fatal errors where the script must exit as quickly as possible. Further use of ConnectPHP will result in ConnectAPIErrorFatal exceptions being thrown. ConnectAPIErrorBase or Exception will catch either ConnectAPIError or ConnectAPIErrorFatal. Test the severity property for RNCPHP\ConnectAPIError::SeverityAbortImmediatly and/or use the instanceof operator to distinguish in the catch block. Custom Objects ConnectPHP presents Custom Objects as the PackageName\ClassName under the versioned namespace. E.g. the RMA class in the CO package of the custom objects included/created with development’s “create_test_db” or “rnt-cocreate.sh”: $a_new_rma = new RightNow\Connect\v1\CO\RMA; OR: use RightNow\Connect\v1 as RNCPHP; $a_new_rma = new RNCPHP\CO\RMA; The interface to Custom Objects is versioned The definition of a Custom Object is not versioned. Otherwise, using a Custom Object in CPHP is the same as using any other CPHP object. Fetching and Finding Objects fetch( $ID [, $options ] ) is available as a static method for any primary object: $aContact = RNCPHP\Contact::fetch( 1, RNCPHP\RNObject::VALIDATE_KEYS_OFF ); Specify VALIDATE_KEYS_OFF when the ID is likely to be a valid ID, or if the code is prepared to catch an “ErrorInvalidID” exception upon the first non-key property access. Using VALIDATE_KEYS_OFF can save one database hit and about 1ms in addition to avoiding one-time ROQL initialization. fetch() may specify a LookupName instead of an ID. VALIDATE_KEYS_OFF is moot since the act of looking up the name validates the ID. Still slower since the name must be looked up. May throw an exception with ErrorInvalidID if the name does not uniquely match exactly one ID. first( $query ) and find( $query ) are available as static methods for any primary object. The $query parameter is the ROQL “WHERE” clause. first() will return the first object of the given type that matches the query, or NULL if none are found, e.g.: RNCPHP\Contact::fetch( 1 ) === RNCPHP\Contact::first( 'ID = 1' ); find() will return the array of objects found to match the given $query: $carr = RNCPHP\Contact::find( 'ID = 1' ); $carr[0] === RNCPHP\Contact::fetch( 1 ); Creating and Saving Objects The save( [ $options ] ) method is available upon primary objects and is used to create or update the system state of a primary object. $options may be: • RNCPHP\RNObject::SuppressExternalEvents • RNCPHP\RNObject::SuppressRules • RNCPHP\RNObject::SuppressAll Nested references to primary objects are not traversed! Any newly created nested primary objects must be save()’d first to avoid an exception from being thrown. $a_inc = RNCPHP\Incident::fetch( 1 ); $a_inc->PrimaryContact = new RNCPHP\Contact(); … $a_inc->PrimaryContact->save(); $a_inc->save(); Use “new” to instantiate a new object: $a_new_org = new RNCPHP\Organization; The ID property is read-only and is NULL upon a new instance until it is successfully save()’d: assert( is_null( $a_new_org->ID ) ); After filling any necessary required fields, save() the instance to create it in the system and to give it an ID: $a_new_org->Name = 'The New Name'; $a_new_org->save(); // just to demonstrate that the ID is there: assert( 0 < $a_new_org->ID ); Destroying Objects destroy( [$options ] ) is a method on primary objects. Can’t get much easier: $org = RNCPHP\Organization::fetch( 1 ); $org->destroy(); $options are the same as for save(). Be prepared to catch Exceptions. Rollback & Commit The results of save() and destroy() operations are implicitly commit upon the successful completion (0 exit status) of the script. Or, a script may explicitly invoke rollback() and/or commit(): RNCPHP\ConnectAPI::rollback(); RNCPHP\ConnectAPI::commit(); Once commit(), there’s no rolling-back; Be careful when catching exceptions Catching an exception and continuing may induce a commit() if the script exits successfully. If you want to rollback upon an exception, gotta do it yourself or coerce a non-zero exit status of the script. Using Sub-Objects Sub-objects in ConnectPHP are essentially nested structures contained within another object. They are not crud-able. I.e., they do not have fetch(), first(), find(), save() or destroy() methods on them. Access them just as any other property: $contact = RNCPHP\Contact::fetch( 1 ); $contact->Name = new RNCPHP\PersonName; $contact->Name->First = 'First'; $contact->Name->Last = 'Last'; Don’t want to bother with looking up the property type name of nested objects? Was that a PersonName or a PersonFullName? Use ConnectPHP metadata. It’s also forward-compatible should the type name change: $md = RNCPHP\Contact::getMetadata(); $contact->Name = new $md->Name->type_name; Using Arrays Lists in the Connect Common Object Model are presented by ConnectPHP as ConnectArray objects implementing the PHP ArrayAccess interface. Such properties are marked in the metadata of the property with a “true” value on the is_list property of the metadata: $md = RNCPHP\Organization::getMetadata(); assert( true === $md->Addresses->is_list ); Access the elements of these properties just as you would an array: $org= RNCPHP\Organization::fetch( 1 ); $org->Addresses[0]->AddressType->ID; count() works: count( $org->Addresses ); “foreach” works but use “for” instead, especially if you’re not going to iterate over the entire array. Order is not guaranteed. Using Arrays (cont’d) Nillable lists are nullable Assigning a list to NULL will empty it: $org->Addresses = NULL; Cannot insert elements – can only append: $org->Addresses[] = new RNCPHP\TypedAddress; Modifying an element property will cause it to be updated upon the next save() of the root primary object. Remove elements using the offsetUnset() method: $org->Addresses->offsetUnset( 0 ); Use “new” to create a new list or to replace an old one: $org->Addresses = new RNCPHP\TypedAddressArray; Or: $org->Addresses = new $md->Addresses->type_name; Using File Attachments Properties that contain FileAttachments vary a bit in flavor, but FileAttachment items all derive from the FileAttachment class. $con = RNCPHP\Contact::fetch( 1 ); $con->FileAttachments = new RNCPHP\FileAttachmentCommonArray; $fa = new RNCPHP\FileAttachmentCommon; There are two ways to begin with adding a file attachment: Via the setFile() method: // Assumes that $tmpfname is an existing file // in the “tmp” folder for the site: $fa->setFile( $tmpfname ); Via the makeFile() method: // Gets a file resource for the script to write to: $fp = $fa->makeFile(); fwrite( $fp, __FUNCTION__." writing to tempfile\n" ); fclose( $fp ); From there, wrap it up with: $fa->ContentType = 'text/plain'; // Set the content type $fa->FileName = 'SomeName.suffix'; // Give it a name $con->FileAttachments[] = $fa; // Append to the list $con->save(); Using File Attachments (cont’d) File data is not directly exposed via ConnectPHP. However, a URL to the file is exposed. E.g. from the example on the previous slide: assert( false !== strpos( $con->FileAttachments[0]->URL, "/{$con->FileAttachments[0]->ID}/" ) ); To present administrative access to a URL, the FileAttachment object has a getAdminURL() method: $con->FileAttachments[0]->getAdminURL(); Using NamedIDs NamedID’s are a way of mapping between names and IDs upon a given property. Like RNObjects, the first two properties are ID and LookupName. Unlike RNObjects, NamedID’s are not crud-able and have no fetch(), first(),f ind(), save() or destroy() methods. Code may set either the ID or the LookupName of a NamedID. But not both! Once one is set, the other becomes read-only. Many properties that are NamedID’s in ConnectPHP are candidates to eventually become primary object references. E.g. Country. Accessing the ID or LookupName of a NamedID or primary object is the same. Only setting them is different. fetch(), first(), find() or save() for primary objects vs direct assignment for NamedID’s. The set of possible NamedID pairs available upon a given property can be discovered from the metadata: $md = RNCPHP\Account::getMetadata(); $md->Country->named_values; // an array of NamedID’s OR: RNCPHP\ConnectAPI::getNamedValues( 'RightNow\\Connect\\v1\\Account', 'Country' ); OR: RNCPHP\ConnectAPI::getNamedValues( 'RightNow\\Connect\\v1\\Account.Country' ); Using NamedIDs (cont’d) The values for a NamedID are tied to it’s context. The LookupName and ID properties get their context from the container hierarchy. Otherwise, it’s just a NamedID and there is no context to allow mapping between the ID and LookupName. I.e. The Country property of an Account object is a NamedID. This works: $md = RNCPHP\Account::getMetadata(); $nid = new $md->Country->type_name; $nid->LookupName = 'US'; $acct = $cphp_Account::fetch( 1 ); $acct->Country = $nid; assert( 1 === $acct->Country->ID ); This doesn’t: $md = RNCPHP\Account::getMetadata(); $nid = new $md->Country->type_name; $nid->LookupName = 'US'; assert( 1 === $nid->ID ); NamedID Flavors There are several flavors of NamedID’s. From a CPHP scripts’ point of view, the distinction is in the type name only, not in functionality. However, scripts must currently take care to assign the proper NamedID type when assigning a new NamedID to a property. This is most easily accomplished by using the metadata to discover the type: $acct = RNCPHP\Account::fetch( 1 ); $md = RNCPHP\Account::getMetadata(); $acct->Country = new $md->Country->type_name; $acct->Country->LookupName = 'US'; assert( 1 === $acct->Country->ID ); NamedID Hierarchies Much like NameIDs, but have a Parents array. The “leaf” is the ID and LookupName immediately on the NamedIDHierarchy object. The “root”, or top of the hierarchy, is Parents[0]. $opp = $cphp_Opportunity::first( 'Territory IS NOT NULL' ); $idPath = array(); $namePath = array(); $max = count( $opp->Territory->Parents ); for ( $ii = 0; $ii < $max; $ii++ ) { $idPath[] = $opp->Territory->Parents[$ii]->ID; $namePath[] = $opp->Territory->Parents[$ii]->LookupName; } $idPath[] = $opp->Territory->ID; $namePath[] = $opp->Territory->LookupName; echo( "idPath= /".join( '/', $idPath )."\n" ); // e.g. /1/2/3 echo( "namePath= /(".join( ')/(', $namePath ).")\n" ); // e.g. /(a)/(b)/(c) In version 1 of ConnectPHP, only found on SalesProduct.Folder, Opportunity.Territory, and the Source property on various objects. Like some NamedID properties, many NamedIDHierarchy properties are candidates to become primary objects in future versions. Compared to CWS, most NamedIDHierarchies in CWS are object references in ConnectPHP. In these cases there is also a read-only array on the object to represent the hierarchy, e.g.: $org = RNCPHP\Organization::first( 'Parent IS NOT NULL' ); $idx = count( $org->OrganizationHierarchy ) - 1; assert( $org->Parent->ID === $org->OrganizationHierarchy[ $idx ]->ID ); Using ROQL Object Queries Use ROQL directly if fetch(), first(), and find() cannot do what you need. ROQL::queryObject( $queries ) returns a ROQLResultSet of the objects found to match the given queries, or NULL if nothing matched. The next() method upon the ROQLResultSet returns the ROQLResult of the next query, or NULL if there are no remaining results. The next() method upon the ROQLResult returns the primary object, or NULL if there are no remaining objects in the result. $rrs = RNCPHP\ROQL::queryObject( 'select contact from contact where id = 1' ); $rs = $rrs->next(); $obj = $rs->next(); assert( RNCPHP\Contact::fetch( 1 ) === $obj ); Using ROQL Tabular Queries Use ROQL directly if fetch(), first(), and find() cannot do what you need. ROQL::query( $queries ) returns a ROQLResultSet of the rows found to match the given queries, or NULL if nothing matched. Similar machinations as with ROQL::queryObject(), but returns rows of tabular data instead of primary objects: $rrs = RNCPHP\ROQL::query( 'select id from contact where id = 1' ); $rs = $rrs->next(); $row = $rs->next(); // Tabular results are strings // Though column/property names // are case-insensitive in ROQL, // they are not so in PHP: assert( '1' === $row[‘ID'] ); Using metadata Metadata is available for every property in every CPHP object Access metadata using the getMetadata() static method on the corresponding class.: $md = RNCPHP\Account::getMetadata(); Property metadata is accessed by property name: $md->Country Interesting metadata on a property: type_name • The fully qualified PHP type name, including namespace , of an object. COM_type • The type of the property in the Connect Common Object Model. is_list • True if the property is a list. is_nillable • True if the property is nillable. is_object • True if the property is an object. is_primary • True if the property is a reference to a primary object. Using metadata (cont’d) Interesting metadata on a property (cont’d): description default • The default value, if any. NULL if no default. constraints • An array of objects representing the constraints on the property. • Each array element is an object with the properties: – kind » May be one of: • RNCPHP\Constraint::Min • RNCPHP\Constraint::Max • RNCPHP\Constraint::MinLength • RNCPHP\Constraint::MaxLength • RNCPHP\Constraint::In • RNCPHP\Constraint::Not • RNCPHP\Constraint::Pattern – value » The value of the constraint Using metadata (cont’d) Visibility: is_read_only_for_create • True if the property must not be set when save()-ing a new object. is_read_only_for_update • True if the property must not be modified when save()-ing an existing object. is_required_for_create • True if the property must be set when save()-ing a new object. is_required_for_update • True if the property must be set when save()-ing an existing object. is_write_only • The property is not read-able (e.g. NewPassword). Gotchyas Misspelling property names Property names are case sensitive. No exceptions are thrown if assigning or accessing a misspelled property No try/catch block Will yield an ugly fatal error should an exception be thrown. Calling exit(0), die( 0 ), or die( “…” ) in a catch block will commit. Only save()’d changes are commit. Merely changing a property does not cause it to be saved to the system. The save() method on the root primary object must be invoked. Don’t use foreach to iterate over CPHP object properties. You’ll get more than you bargained for. Use get_class_vars() instead. print_r() doesn’t get everything. print_r() only sees as non-null those properties that have been previously accessed since the last save(). print_r() does spew out everything on metadata. Object instances of the same type with the same ID are essentially global in scope. Modifying a property via one object reference is seen by all references to the same object, even if fetch()’d again. Using a reference to a property of a CPHP object yields a reference to the class variable, not the property value on the object. Best Practices for ConnectPHP in CP Keep “heavy lifting” in the model. Use a “use” statement to declare an alias to the version of ConnectPHP to target. Avoid using things that require that the versioned namespace be used or declared in a widget. But if you must, use a “use” statement to declare an alias that is unique to the widget to avoid errors in the staged or deployed instance. Avoid unnecessary copying of CPHP objects & properties. Pass the CPHP object to the widget instead of copying properties. Avoid using CPHP objects as “scratch pads”. CPHP primary object instances are global. “hitting” a CPHP property is slower than using a PHP variable. Use VALIDATE_KEYS_OFF on the fetch() method. ~120us vs ~1500us (1st time for same type/ID) Avoids a “database hit”. Avoids ROQL initialization. Wrap blocks of code with try/catch. Prefer using ID’s when possible instead of LookupName to fetch() an object or to specify a NamedID. Using LookupName instead of the ID may incur an extra db hit. I.e. present “LookupName” to user, but map to ID at the client. Using ROQL … Use ROQL instead of direct SQL and when fetch(), first() and find() cannot do what you need. If you can, use fetch() with VALIDATE_KEYS_OFF instead to avoid ROQL one-time initialization. ROQL one-time initialization: ~50ms Other Resources ConnectPHP Documentation: TBA Look for it in the Technical Library ConnectPHP 2010 Developer Conference slides: http://rightnow.com/resource-slides-dc-desktop-connect-for-php.php ROQL 2010 Developer Conference slides: http://rightnow.com/resource-slides-dc-rightnow-object-query-languageroql.php ROQL Demo (using ConnectPHP and CP): http://hd-10-11.dx.lan/app/demo/roql Example CP Model: http://connect-php.marias.rightnowtech.com/app/using_cphp_in_cp A work-in-progress. Custom Schema to Custom Object Upgrades Training Includes an example of using ConnectPHP to work with Custom Objects that were upgraded from Custom Schema Tuesday December 14th, 3-4pm Q&A Reach me at: Mark.Rhoads@RightNow.com