290 likes | 420 Views
Connect for PHP. Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API. Key Features. Object-oriented binding of the RightNow Connect Common Object Model (CCOM) for PHP Uses namespaces for versioning
E N D
Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API
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? • UseConnectPHP 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. • TheLookupName 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 forConnectPHP 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 forsame 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. • UsingLookupName 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-language-roql.php • ROQL Demo (usingConnectPHP 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