Connect_for_PHP_UpgTraining

advertisement
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
Download