Thursday, September 2, 2010

Microsoft Dynamics CRM and PHP

I needed to use PHP for two things:

  • Given a lead's email address, retrieve the name, title, phone and email address of this lead's owner.
  • Insert a new lead into the system.

I spent a lot of time searching the web, and saw many posts of people being unsuccessful with using SoapClient, so I didn't even bother trying. The first order of business was to figure out how to login using IFD (Internet Facing Deployment.) I found this page, which explained how to authenticate. It didn't work, but eventually we figured out that our IFD was not configured properly. In the IFD configuration tool, you can't just leave the "local" subnets blank. At least one IP range must be added, even if you want everything to use IFD. We added 127.0.0.1-127.0.0.255 as the "local" range.

I wrote two classes to handle the CRM. These are in the mscrm.php file.

<?
// vim: filetype=php

class MSCRMServiceException extends Exception {}
class MSCRMLoginFailed extends MSCRMServiceException {}

class MSCRMService {
  protected $host, $org, $domain, $user, $password, $curl_handle, $ticket;

  function __construct($crm_host, $organization, $domain, $user, $password) {
    $this->host = $crm_host;
    $this->org = $organization;
    $this->domain = $domain;
    $this->user = $user;
    $this->password = $password;
    $this->init_curl();
    $this->login();
  }

  function __destruct() {
    curl_close($this->curl_handle);
  }

  public function domain_user() {
    return $this->domain . '\\' . $this->user;
  }

  protected function init_curl() {
    $this->curl_handle = curl_init();
    curl_setopt($this->curl_handle, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($this->curl_handle, CURLOPT_CONNECTTIMEOUT, 10);
    curl_setopt($this->curl_handle, CURLOPT_TIMEOUT, 30);
    curl_setopt($this->curl_handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  }

  protected function login() {
    $request = '<?xml version="1.0" encoding="utf-8"?' . '>
      <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <soap:Body>
          <Execute xmlns="http://schemas.microsoft.com/crm/2007/CrmDiscoveryService">
            <Request xsi:type="RetrieveCrmTicketRequest">
              <OrganizationName>' . $this->org . '</OrganizationName>
              <UserId>' . $this->domain_user() . '</UserId>
              <Password>' . $this->password . '</Password>
            </Request>
          </Execute>
        </soap:Body>
      </soap:Envelope>';
    $headers = array(
      'Host: ' . $this->host,
      'Connection: Keep-Alive',
      'SOAPAction: "http://schemas.microsoft.com/crm/2007/CrmDiscoveryService/Execute"',
      'Content-type: text/xml; charset="utf-8"',
      'Content-length: ' . strlen($request)
    );
    curl_setopt($this->curl_handle, CURLOPT_URL, 'http://' . $this->host . '/MSCRMServices/2007/SPLA/CrmDiscoveryService.asmx');
    curl_setopt($this->curl_handle, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($this->curl_handle, CURLOPT_POST, true);
    curl_setopt($this->curl_handle, CURLOPT_POSTFIELDS, $request);

    $response = curl_exec($this->curl_handle);
    $matches = array();
    if(preg_match('/<CrmTicket>([^<]*)<\/CrmTicket>/', $response, $matches)) {
      $this->ticket = $matches[1];
    } else {
      throw new MSCRMLoginFailed;
    }
  }

  public function xml_request($xml, $service, $action) {
    $request = '<?xml version="1.0" encoding="utf-8"?' . '>
      <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <soap:Header>
          <CrmAuthenticationToken xmlns="http://schemas.microsoft.com/crm/2007/WebServices">
            <AuthenticationType xmlns="http://schemas.microsoft.com/crm/2007/CoreTypes">2</AuthenticationType>
            <OrganizationName xmlns="http://schemas.microsoft.com/crm/2007/CoreTypes">' . $this->org . '</OrganizationName>
          </CrmAuthenticationToken>
        </soap:Header>
        <soap:Body>
          <' . $action . ' xmlns="http://schemas.microsoft.com/crm/2007/WebServices">
            ' . $xml . '
          </' . $action . '>
        </soap:Body>
      </soap:Envelope>';
    $headers = array(
      'Host: ' . $this->host,
      'Connection: Keep-Alive',
      'SOAPAction: "http://schemas.microsoft.com/crm/2007/WebServices/' . $action . '"',
      'Content-type: text/xml; charset="utf-8"',
      'Content-length: ' . strlen($request)
    );
    curl_setopt($this->curl_handle, CURLOPT_URL, 'http://' . $this->host . '/MSCrmServices/2007/' . $service . '.asmx ');
    curl_setopt($this->curl_handle, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($this->curl_handle, CURLOPT_POST, true);
    curl_setopt($this->curl_handle, CURLOPT_POSTFIELDS, $request);
    // Ticket goes into a cookie.
    curl_setopt($this->curl_handle, CURLOPT_COOKIE, 'MSCRMSession=ticket=' . $this->ticket);
    $response = curl_exec($this->curl_handle);
    return $response;
  }

  // Retrieves given $attributes for given $entity where $condition_attribute equals $condition_string.
  public function retrieve_multiple($entity, $attributes, $condition_attribute, $condition_string) {
    $attributes_xml = "";
    foreach($attributes as $attribute) {
      $attributes_xml .= "<q1:Attribute>" . htmlspecialchars($attribute) . "</q1:Attribute>\n";
    }
    $xml = '
      <query xmlns:q1="http://schemas.microsoft.com/crm/2006/Query" xsi:type="q1:QueryExpression">
        <q1:EntityName>' . htmlspecialchars($entity) . '</q1:EntityName>
        <q1:ColumnSet xsi:type="q1:ColumnSet">
          <q1:Attributes>
            ' . $attributes_xml . '
          </q1:Attributes>
        </q1:ColumnSet>
        <q1:Distinct>false</q1:Distinct>
        <q1:Criteria>
          <q1:FilterOperator>And</q1:FilterOperator>
          <q1:Conditions>
            <q1:Condition>
              <q1:AttributeName>' . htmlspecialchars($condition_attribute) . '</q1:AttributeName>
              <q1:Operator>Equal</q1:Operator>
              <q1:Values>
                <q1:Value xsi:type="xsd:string">' . htmlspecialchars($condition_string) . '</q1:Value>
              </q1:Values>
            </q1:Condition>
          </q1:Conditions>
        </q1:Criteria>
      </query>
    ';
    $response_xml = $this->xml_request($xml, "CrmService", "RetrieveMultiple");
    $response = array();
    foreach($attributes as $attribute) {
      $matches = array();
      if(preg_match('/<q1:' . $attribute . '[^>]*>([^<]*)<\/q1:' . $attribute . '>/', $response_xml, $matches)) {
        $response[$attribute] = $matches[1];
      }
    }
    return $response;
  }

  // Create an entity.  $attributes should have attributes as keys and their values as values.
  public function create_entity($entity, $attributes) {
    $xml = "<entity xsi:type=\"$entity\">\n";
    foreach($attributes as $k => $v) {
      $xml .= "<$k>" . htmlspecialchars($v) . "</$k>\n";
    }
    $xml .= "</entity>\n";
    return $this->xml_request($xml, "CrmService", "Create");
  }

  // Usage: $options = retrieve_attribute("lead", "leadsourcecode")
  public function retrieve_attribute($entity, $attribute) {
    $xml = '
      <Request xsi:type="RetrieveAttributeRequest">
        <EntityLogicalName>' . $entity . '</EntityLogicalName>
        <LogicalName>' . $attribute . '</LogicalName>
      </Request>
    ';
    $xml = $this->xml_request($xml, "MetadataService", "Execute");
    // Get a list of <Option> tags.
    $matches = array();
    if(!preg_match_all('/<Option>(.*?)<\/Option>/', $xml, $matches)) {
      return null;
    }
    $options_xml = $matches[1];
    $options = array();
    foreach($options_xml as $option_xml) {
      $matches = array();
      if(!preg_match('/<Value[^>]*>([^<]*)<\/Value>/', $option_xml, $matches)) {
        continue;
      }
      $code = $matches[1];
      if(!preg_match('/<Label>([^<]*)<\/Label>/', $option_xml, $matches)) {
        continue;
      }
      $name = $matches[1];
      $options[] = array("code" => $code, "name" => $name);
    }
    return $options;
  }

  public function find_code_by_name($entity, $attribute, $name) {
    $codes = $this->retrieve_attribute($entity, $attribute);
    foreach($codes as $code) {
      if($code["name"] == $name) {
        return $code["code"];
      }
    }
    return null;
  }
}

class MSCRMLead {
  protected $service;

  function __construct($service) {
    $this->service = $service;
  }

  public function find_user($guid) {
    $retrieve_fields = array("firstname", "lastname", "title", "internalemailaddress", "address1_telephone1");
    return $this->service->retrieve_multiple("systemuser", $retrieve_fields, "systemuserid", $guid);
  }

  public function find_lead_by_email($email) {
    $retrieve_fields = array("leadsourcecode", "firstname", "lastname",
                             "jobtitle", "companyname", "industrycode", "websiteurl", "address1_telephone1", "emailaddress1",
                             "address1_city", "address1_stateorprovince", "address1_country", "ownerid");
    return $this->service->retrieve_multiple("lead", $retrieve_fields, "emailaddress1", $email);
  }

  public function find_owner_by_email($email) {
    $lead_info = $this->find_lead_by_email($email);
    if(!$lead_info["ownerid"]) {
      return null;
    }
    $owner = $this->find_user($lead_info["ownerid"]);
    return $owner;
  }

  public function find_leadsourcecode($leadsource) {
    return $this->service->find_code_by_name("lead", "leadsourcecode", $leadsource);
  }

  public function find_industrycode($industry) {
    return $this->service->find_code_by_name("lead", "industrycode", $industry);
  }

  public function create($lead_info) {
    return $this->service->create_entity("lead", $lead_info);
  }
}

?>

You can now retrieve a lead's owner information like this:

require_once("mscrm.php");

$mscrm_service = new MSCRMService($host, $organization, $domain, $user, $password);
$mscrm_lead = new MSCRMLead($mscrm_service);
$owner = $mscrm_lead->find_owner_by_email($lead_email);

Creating a new lead is a little more involved, since you need to look up codes for picklists, like leadsourcecode. You can do something like this:

require_once("mscrm.php");

$mscrm_service = new MSCRMService($host, $organization, $domain, $user, $password);
$mscrm_lead = new MSCRMLead($mscrm_service);

$lead_info = array();

$leadsourcecode = $mscrm_lead->find_leadsourcecode($lead_source);
if($leadsourcecode) {
  $lead_info["leadsourcecode"] = $leadsourcecode;
}

$lead_info["firstname"] = $firstname;
$lead_info["lastname"] = $lastname;
$lead_info["jobtitle"] = $jobtitle;
$lead_info["companyname"] = $companyname;

$industrycode = $mscrm_lead->find_industrycode($industry);
if($industrycode) {
  $lead_info["industrycode"] = $industrycode;
}

$lead_info["websiteurl"] = $websiteurl;
$lead_info["address1_telephone1"] = $phone;
$lead_info["emailaddress1"] = $email;
$lead_info["address1_city"] = $city;
$lead_info["address1_stateorprovince"] = $state;
$lead_info["address1_country"] = $country;

$mscrm_lead->create($lead_info);
Hopefully this will save someone days of headaches.

10 comments:

JP said...

Thanks in advance for the post. I want to give it a try - but do I run the php on a windows server, the same server where CRM is running? I was hoping to run it on Fedora or Red Hat ... and ... do I need to compile php with anything special?

Gary said...

PHP does not need to run on the same server as your CRM.

Tim Joseph said...

Where do I find my host information and other login details for my crm?

Gary said...

The host and login information for your CRM should available from whoever set up your CRM. It may not be a bad idea to have them create a separate account for your application that only has sufficient privileges for what the application needs to do.

Wes said...

Is there a simple command to verify that you are connected to the CRM?

_ _ said...

I just found your post and think I've got everything nearly working except I get "Uncaught exception 'MSCRMLoginFailed'" when trying to run the script. Can you give examples for the $host, $organization and $domain values that you use to connect?

KAVITA JAGTAP said...

I also got same LOGIN error, CONNECTION FAILED..

Is host name is must? I enetred all values except host name.

Please guide for connection function

Priya sen said...

I also got the same Fatal error: Uncaught exception'MSCRMLoginFailed'
error. Can you give examples for the $host, $organization and $domain ans $user, $password values that you use to connect?

ajeesh tp said...

Hi,

Can you please tell detail with regards the connection details.?

$this->host = "";
$this->org = "";

ajeesh tp said...

I also got this error message.

Fatal error: Uncaught exception 'MSCRMLoginFailed'