smith-carson/prosperworks-sdk

This package is abandoned and no longer maintained. No replacement package was suggested.

Unofficial (but pretty decent) SDK for the ProsperWorks CRM API

1.0.18 2018-04-03 15:25 UTC

This package is auto-updated.

Last update: 2021-07-01 15:39:14 UTC


README

layout
default

Introduction

At the time of implementation, there was no SDK for ProsperWorks, and we needed to do a bunch of operations to transfer data from our old CRM and synchronize information with the other subsystems, so we designed some classes to encapsulate the API operations. It uses Guzzle, but ended up overgrowing and we turned into a standalone library.

This project was originally written by igorsantos07 and is now maintained by smith-carson (website).

Installation

1. Install in your project

You need to be running PHP 7 (yep, it's stable since dec/2015 and packs a bunch of useful features, upgrade now!).

To add it to your project through Composer, run composer require igorsantos07/prosperworks. It will get the most stable version if your min-requirements are "stable" or dev-master otherwise.

2. Configure the package

There's a couple of things to configure to get the SDK running:

API credentials

Currently, there's no "API User" on ProsperWorks, so you need an active user to communicate with the API.

  1. Sign in to ProsperWorks, go to Settings > Preferences / API Keys. There you will be able to generate a new API Key for the logged user. Copy that key, along with the user email.

  2. Create a bootstrap file, or include the following code in the config section of your project: \ProsperWorks\Config::set($email, $token).

Webhooks parameters

[Optional] If you're going to use Webhooks to get updates from ProsperWorks, you'll also need to feed in three more arguments on that method:

  1. A Webhooks Secret, that will be used to avoid unintended calls to your routes. That should be a plain string.
  2. A Root URL. That's probably the same domain/path you use for your systems, and what ProsperWorks will POST to. More information on the Webhooks section.
  3. A Cryptography object. It should respond to encryptBase64() and decryptBase64(), both receiving and returning a string (it can also implement \ProsperWorks\Interfaces\Crypt to make things easier). It will be used to send an encrypted secret, and decrypt it to make sure the call you receive comes from ProsperWorks (or, at least, someone that has the encrypted secret).

Caching object

[Optional] To make some parts faster, you can also feed the sixth argument with a caching layer. It's an object that needs to respond to get() and save(), or implement \ProsperWorks\Interfaces\Cache.

It's mainly used to cache (for an hour) meta-data from the API, such as Custom Fields, Activity and Contact Types, and so on. That's information that rarely changes so it's safe to cache, making calls much faster (otherwise, for every resource with custom fields we would need to retrieve from the custom fields endpoint as well).

3. Debug mode

During import scripts and similar tasks it could be useful to peek into the network traffic and see if what you intended to do is being done correctly. You can enable echo's of debug information from the library by calling ProsperWorks\Config::debugLevel():

ProsperWorks\Config::DEBUG_BASIC
will trigger some messages, such as "POST /people/search" so you know which requests are being sent. It also warns on Rate limits being hit.
ProsperWorks\Config::DEBUG_COMPLETE
does all above plus complete requests payload.
null, false, 0 or ProsperWorks\Config::DEBUG_NONE
will stop printing messages.

This doesn't need to be done together with Config::set(); it can happen anywhere and will change behavior from that part on.

Tip: "sandbox" account

After a while, when implementing this library for the first time, we spoke with a support representative about the lack of a sandbox environment. They suggested us to create a trial account and use that instead of a user on the paying account, and mention to the Support that was being used to test-drive the API implementation - and thus, they would extend the trial of that solo account for as long as it was needed.

API Communication

Most of the operations are done through the \ProsperWorks\CRM abstract class, and the resulting objects from it (you can consider it some sort of Factory class). The exception are Webhooks, that have a special Endpoint class to make it easier.

Tip: ProsperWorks API Documentation
You may want to read the REST API Docs, to get an understanding of the inner pieces that make up this SDK.

With configurations in place, ProsperWorks API calls are done through a simple, fluent API. Most of the endpoints behave the same way, with special cases being the Account and most meta-data endpoints.

On the following examples we'll consider the classes were imported in the current namespace.

Common endpoints

Singular, empty static calls to CRM give an Endpoint object (see saving instances), that allows you to run all common operations:

<?php
//runs GET /people/10 to retrieve a single record
$people = CRM::person()->find(10);

//runs GET /people multiple times (it's paged) until all entries are retrieved
$people = CRM::person()->all();
//there's no such operation in some endpoints; all() runs an empty /search, instead

//runs POST /people to generate a new record
$newPerson = CRM::person()->create(['name' => 'xxx']);

//runs PUT /people/25 to edit a given record
$person = CRM::person()->edit(25, ['name' => 'xxx']);

//runs DELETE /people/10 to destroy that record
$bool = CRM::person()->delete(10);

//runs POST /people/search with the given parameters until all entries are found (it's paged)
$people = CRM::person()->search(['email' => 'test@example.com']);

All success calls will return a BareResource object, with all information from that endpoint, or a list of those. See Response Objects for details.
If it fails, an error message is given. (TODO: option to raise exceptions)

There are also some shortcuts, such as:

<?php
//plural calls do the same as the singular all() call
$people = CRM::people();       //same as CRM::person()->all()
$tasks = CRM::tasks();         //same as CRM::task()->all()
$companies = CRM::companies(); //same as CRM::company()->all()

//there's also two other types of magic calls
$people = CRM::person(23);                  //same as CRM::person()->find(23)
$people = CRM::person(['country' => 'US']); //same as CRM::person()->search(...)

Special cases: restricted endpoints

All meta-data resources (called Secondary Resources on the docs), together with the Account endpoint, have only read access. There's no verification of valid operations yet (see #7). Here's a list of those read-only endpoints, accessible through the plural call (e.g. CRM::activityTypes()), except for Account which is singular:

  • Account (the only one you have to call in the singular)
  • Activity Types
  • Contact Types
  • Custom Fields
  • Customer Sources
  • Loss Reasons
  • Pipelines
  • Pipeline Stages

Meta-data shortcuts

As those endpoints are mostly lists, you can also access that data through the cacheable CRM::fieldList() method, which returns the information in a more organized fashion:

<?php
$types = CRM::fieldList('contactType'); //singular!
print_r($types);
// gives an array of names, indexed by ID:
// (
//     [123] => Potential Customer
//     [124] => Current Customer
//     [125] => Uncategorized
//     [126] => Former Customer
// )

echo CRM::fieldList('contactType', 524131); //search argument
// prints "Potential Customer". That argument searches on both sides of the array

$actTypes = CRM::fieldList('activityType', null, true); //asks for "detailed response"
print_r($actTypes);
// gives the entire resources, still indexed by ID
//     [166] => stdClass Object
//         (
//             [id] => 166
//             [category] => user
//             [name] => Social Media 
//             [is_disabled] => 
//             [count_as_interaction] => 1
//      )
//
//     [...]
// )

Sanity warning: those IDs there are samples; they're different for each ProsperWorks customer.

It's also worth noting that some fields are "translated" from the API into specific objects, such as timestamps, addresses, Custom Fields and more, so you'll probably never have to deal with the Custom Fields endpoint directly. More information about that on the SubResources and Response Objects sections.

Related Items

There's an unified API to created links between two resources. Thus, every Resource object has its own related method, to manipulate those links. As that's a very simple API, you can only list, create and delete relationships. Take a look on the Documentation for Related Items to see the relation limits - some resources allow for only one link, and not every resource has relationships with every other.

<?php
// you always have to feed the origin resource ID to related()
// and then call the operation you want, like:
$tasks = CRM::task()->related(10)->all();              //lists all
$task_projects = CRM::task()->related(22)->projects(); //lists specific type
$task_project = CRM::task()->related(22)->create(10, 'project'); //create one
$task_project = CRM::task()->related(22)->delete(27, 'project'); //and remove

Batch Operations

It's also possible to run batch operations, using Guzzle's concurrency features to speed up with parallel calls. Some single-usage methods have a *Many counterpart, such as:

createMany()
straightforward; instead of a payload, you pass a list of payloads
editMany()
in this case, you got to pass a list of payloads, indexed by IDs.
delete()
is special, as it can handle an arbitrary number of IDs. Its response will vary on the number of arguments.

You can use an array, Interator or Generator on these, and it will take care to run as much as 10 (future: configurable) HTTP calls at the same time.

As an example, let's create a lot of Task entries, based on a query result (that also has low memory usage), and then remove these:

<?php
//this call is using a simple generator
$thousandsOfTasksQueryResult = [...];
$allTasks = CRM::task()->createMany(function() use ($thousandsOfTasksQueryResult) {
    foreach ($thousandsOfTasksQueryResult as $task) {
        yield [
            'name' => $task->name,
            'due_date' => $task->dueDate->format('U'),
            'status' => $task->completed? 'Completed' : 'Open'
        ];
    }
});

// as that's a batch operation, it seemed unsafe to throw harsh errors.
// thus, success will give an object of data, while errors return a simple message
$toDelete = [];
foreach ($allTasks as $response) {
    if (is_object($response) {
        $toDelete[] = $response->id;
    } else {
        $logger->warning("Couldn't create Task: $response");
    }
}

//here we use a plain list of arguments: you have to unpack the array
CRM::task()->delete(...$toDelete);
}

A generator is specially useful in these cases as it will save you a lot of memory, by not storing a long list of payloads/requests in-memory.

Batch Relationship operations

Similar to batch API calls, it's also possible to run a bunch of relation changes. To do that, use relatedBatch()'s methods, with a list of ID + Type (or the Relation helper object), indexed by origin ID:

<?php
use ProsperWorks\SubResources\Relation;

$relClientsQuery = [...];
CRM::task()->relatedBatch()->create(function() use ($relClientsQuery, $pwTaskId) {
    foreach ($relatedClientsQuery as $client) {
        // this would generate an array of Relation() objects, indexed by the same ID
        // causes no error; this won't become a real array (thus, with keys conflicts)
        yield new $pwTaskId => new Relation($client->id, 'company');

        // the following would also work
        //yield new $pwTaskId => ['id' => $client->id, 'type' => 'company'];
    }
});

I don't think all those static calls are performant

Indeed, on a very small scale, they might not be. You can always use the half-way object to run common operations, as when you're running a bunch of operations on the same endpoint. However, the static calls will save you from a couple of config/instances on one-off calls ;)

<?php
$peopleRes = CRM::person();
$client = $peopleRes->find($clientId);
$tags = array_merge($client->tags, 'new tag');
$peopleRes->edit($clientId, compact('tags'));

Rate limiting

There's also a RateLimit blocker built-in to the SDK, so it will sleep() a bit when it notices a limit would be hit, allowing for new operations shortly after the limit is released. That emits some notices on the CLI when Debug mode is on. This is specially useful for Batch operations.

Response objects

Most (all?) responses will be a BareResource object, or a list of those. The biggest advantage is that class's "translation" capabilities: it makes some parts of the payload easier to use by leveraging Objects with simpler / predictable structures, or with some data validation / translation under the hood (AKA SubResources).

  • most date fields (date_created, due_date, date_last_contacted, ...) will turn from UNIX Timestamps into DateTime objects. There's a not-really-working setting to disable that, on BareResource (see #8)
  • a contact_type field will be generated with the name related to contact_type_id, if any
  • Custom Fields will generate two entries:
    • custom_fields_raw, containing the original payload from the API
    • custom_fields, containing a list of CustomField objects, indexed by field name
  • some other complex structures will also become SubResource objects, such as:

SubResources

There are a couple of dependant objects that are not to be used directly on API calls, but make part of the main resources. Most of the times, those are inner documents inside the JSON payload. They're used on responses (see Response Objects), but they're also designed to make your calls easier, "translating" some information back and forth, and making sure you always follow the requested rules for those sub-documents.

In special, a SubResource implementing the TranslateResource trait will allow read-access to some protected fields (listed in $altFields). In short, when you turn an object into an array on PHP (what we do to get the final JSON payload) it creates an array of all public fields. Thus, a TranslateResource is able to give read access to some "hidden" properties while not exposing that to the API. See the list below for behavior examples:

Address

Accepts as the first argument either a complete line (a string called street, because that's how ProsperWorks does), or an array with two address lines (called address and suite). To change between those two formats a there's a "suite" separator (hint: if there's a "suite" on the suite part already, it won't be repeated ok?). The other arguments are pretty standard, such as city, postal code and so on.

<?php
$sherlock = new Address('221B Baker St. suite 2', 'London');
// same as  new Address(['221B Baker St.', '2'], 'London');
// same as  new Address(['221B Baker St.', 'suite 2'], 'London');
echo $sherlock->street;  //'221B Baker St. suite 2'
echo $sherlock->address; //'221B Baker St.'
echo $sherlock->suite;   //'2'

$nemo = new Address('42 Wallaby Way', 'Sydney');
echo $nemo->street;  //'42 Wallaby Way'
echo $nemo->address; //'42 Wallaby Way'
echo $nemo->suite;   //null

//and then, use at will:
CRM::person()->create([
    'name' => 'P. Sherman',
    'address' => $nemo
]);

Relation

This was shown before: it's a simple holder for id and type, no extra features.

Custom Field

This is the biggest guy. It does the translation between a bare custom field specification (id + value, not really meaningful for humans) and actually readable information. The original field ID (same ID for the same field, even across different types of resources) is stored in custom_field_definition_id, together with the value. There's always a read-only name property, with the field's actual name, and if it's a list, a read-only string valueName will also be filled with the field's readable value.

To create a Custom Field entry, you can use both the field ID or field name as the first argument, and either the value ID or string on the second place. Remember to double-check casing when you use a string instead of the IDs, though!

The following example displays both how to use the class and how it's returned in the SDK responses:

<?php
$person = CRM::person()->create([
    'name' => 'John Doe',
    'custom_fields' => [
        new CustomField('Alias', 'Johnny Doe')
    ]
]);

print_r($person);
// ProsperWorks\Resources\BareResource Object (
//     [id] => 12340904
//     [name] => John Doe
//     [custom_fields] => Array (
//         [Alias] => ProsperWorks\SubResources\CustomField Object (
//             [custom_field_definition_id] => 128903
//             [value] => Johnny Doe
//             [name:protected] => Alias
//             [valueName:protected] =>
//             [...]
//         )
//     )
//     [custom_fields_raw] => Array (
//         [0] => stdClass Object (
//             [custom_field_definition_id] => 128903
//             [value] => Johnny Doe
//         )
//         [1] => stdClass Object (
//             [custom_field_definition_id] => 124953
//             [value] =>
//         )
//     )
//     [...]
// )

"Categorized" sub-resources

All of these classes inherit from Categorized, giving them a category property and a couple of constants to use as values. If, on a child class, a constant is marked as "deprecated", it means it doesn't really work with that object.

Their signature is the same: value as first argument, category as the second one.

Email

Simplest child: has an email property, together with the category.

Phone

The first argument is the string number. But the trick here is you can feed an extension by using something like this: "123-4444 x123", and it will get separated into two read-only fields: simpleNumber and extension.

URL

Feed a valid url as the first argument and a social URL category will be automatically filled, if any matches. For other cases, you can give the category manually as the second argument, as usual.

Webhooks

On the other hand, if you want to get updates from ProsperWorks, you have to setup your endpoints for them to call with any changes that happen.

Tip: you may want to take a look on [Webhooks guide]; it's still being worked on - that's why it's still a KB article.

Available Events

According to the documentation, there are three types of events that you can subscribe to:

Webhooks::EV_NEW
a new entry was created
Webhooks::EV_UPDATE
an entry got updated
Webhooks::EV_DELETE
an entry got deleted
Webhooks::EV_ALL
catch-all constant that will subscribe you to all events at once

And these are available for any of the main endpoints, listed under Webhooks::ENDPOINTS:

  • Company
  • Lead
  • Opportunity
  • Person
  • Project
  • Task

How to interact with the webhooks

There are a couple of methods on the Webhooks class, that you should use on your project's REPL or CLI tool when you configure them, or inside your Controller to manipulate the ProsperWorks calls:

To configure webhooks

You must first instantiate the Webhooks object with the root path for your application's environment, otherwise it will use what's configured as default (if any) by Config. That in-place config ability will come in handy during testing.

Then, you can use a couple of methods to interact with the ProsperWorks Webhooks API:

list(int $id = null)
Returns a list of webhook details, indexed by ID.
create(string $endpoint, $event = self::ALL)
Subscribes a new webhook for the given endpoint and event match, with the secret specified on `Config`. You should use one of the CRM::RES_* constants for the first argument.
delete(int ...$id)
Removes one or more webhooks from the ProsperWorks pool.

To interpret webhook calls

Every time a set event happens on one of the set endpoints, ProsperWorks's servers will make an HTTPS call to an address that is composed like this: <root_path>/prosperworks_hooks/<endpoint>/<event>. Your controllers should somehow be able to interpret that the way best suits your application, but most of that information is also repeated in the payload, so feel free to have a single catch-all action to work on it.

The webhook payload

The payload is plain and simple. It contains the affected/generated IDs (usually a single one, unless that's a batch operation), the affected endpoint and generated event, the webhook subscription ID, a timestamp and our secret.

Before any further operation, you should verify, for security purposes, if the secret is correct. To do that, call validateSecret() with the payload, in array format. It will search for the secret, decrypt and verify it. Returns null when the secret is not present, or false if it's there but isn't valid.

The next step, usually, would be for you to access the ProsperWorks API as usual to gather information on the created / updated resource, and update your database accordingly, or remove whatever needs to be removed.

It's worth saying that, with their current UI, every field change made by the user is automatically saved; that's good for their users, but every of those save calls will make a new webhook call, and that might overwhelm your server.
Thus, a nice idea would be to have some sort of queue system to work on those changes, and maybe even a deboncer that could reduce repeated changes on the same resources - i.e. discarding payloads with the same endpoint, event and IDs that happened in a short period of time.

How to develop with webhooks

An important fact here is that ProsperWorks demands all webhook calls to be encrypted, what means you must provide an HTTPS address for them. Besides that, your webserver must be openly accessible on the web. That might not be the most trivial settings for a development environment, right?

Well, the tip is using ngrok during development. You can run it pretty easily from the command-line and have it open a stable HTTPS tunnel, and give you it's URL; you feed that into the Webhooks object and setup new subscriptions that ProsperWorks can call when you make changes on their UI.
It's also possible to inspect the HTTP traffic using the Ngrok inspector; its URL is also displayed when you run it. That might come in handy so you don't need to edit fields a thousand of times to verify your code is working, as it's able to store and repeat calls it received. Neat, isn't it?