+ All Categories
Home > Documents > © RightNow Technologies, Inc. Connect for PHP Mark Rhoads [email protected] Senior...

© RightNow Technologies, Inc. Connect for PHP Mark Rhoads [email protected] Senior...

Date post: 16-Dec-2015
Category:
Upload: angelina-standish
View: 236 times
Download: 12 times
Share this document with a friend
29
© RightNow Technologies, Inc. Connect for PHP Mark Rhoads [email protected] Senior Developer, API Team Public API
Transcript
Page 1: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

© RightNow Technologies, Inc.

Connect for PHP

Mark [email protected]

Senior Developer, API Team

Public API

Page 2: © RightNow Technologies, Inc. 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” Loadingfetch(), 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.

Page 3: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 FieldsObjects 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 handlingErrors 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 assignmentAnd 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 typingAssigning 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.

Page 4: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 5: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 6: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 errorsUse ->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.

Page 7: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 8: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 );

Page 9: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 );

Page 10: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 11: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 exceptionsCatching 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.

Page 12: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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;

Page 13: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 14: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

Using Arrays (cont’d)

Nillable lists are nullableAssigning 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;

Page 15: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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();

Page 16: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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();

Page 17: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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’sOR: RNCPHP\ConnectAPI::getNamedValues( 'RightNow\\Connect\\v1\\Account', 'Country' );OR:

RNCPHP\ConnectAPI::getNamedValues( 'RightNow\\Connect\\v1\\Account.Country' );

Page 18: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 );

Page 19: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 );

Page 20: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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/3echo( "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 );

Page 21: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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 );

Page 22: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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'] );

Page 23: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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.

Page 24: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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

Page 25: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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).

Page 26: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

Gotchyas

Misspelling property namesProperty names are case sensitive.No exceptions are thrown if assigning or accessing a misspelled property

No try/catch blockWill 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.

Page 27: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

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

Page 28: © RightNow Technologies, Inc. Connect for PHP Mark Rhoads Mark.Rhoads@RightNow.com Senior Developer, API Team Public API.

Other Resources

ConnectPHP Documentation:TBALook 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 (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_cpA work-in-progress.

Custom Schema to Custom Object Upgrades TrainingIncludes an example of using ConnectPHP to work with Custom Objects that were upgraded from Custom SchemaTuesday December 14th, 3-4pm


Recommended