Archive for Tips Category Feed

Calling Apex Web Services from PHP

Apex Code can be exposed as a Web Service and made available outside of your Salesforce environment (e.g. from a PHP page). This approach essentially lets you build a personal API into your Salesforce org and eliminates the need for calling standard API methods in PHP code where it is vulnerable to your configuration changes.

Working with the folks over at MK Partners on some recent projects, I’ve learned how to call into Apex Web Services from PHP. It’s actually pretty easy. Special thanks for Simon Fell for helping me through a particularly tricky part.

Apex Web Service Class

A simple web service class is below. The method we call from PHP is myMethod. There are 2 inner classes that are used to capture inputs and send back outputs to PHP.

global class MyWebService {

    // A class to accept an array of input records (e.g. product/amount combinations)
    global class myInputs{
        webservice Id productId;
        webservice Double amount;
    }
    
    // A class to send back as an output to PHP
    global class myOutputs{
        webservice String errorMessage;
        webservice Boolean success;
        webservice List<myInputs> inputs;
        webservice Id contactId;
    }

    // The actual web service method we will call
    webservice static myOutputs myMethod(Id contactId, List<myInputs> inputs){
    
        /* 
        * Write a bunch of code here to do all kinds of stuff.
        */
        
        myOutputs output = new myOutputs();
            output.errorMessage = 'No errors here.';
            output.success = true;
            output.inputs = inputs;
            output.contactId = contactId;
        return output;
        
    }
}
PHP

Login like you normally would using the PHP toolkit. Nothing new here. The final part is defining some constants for use later when we call the web service.

// Include the PHP Toolkit
require_once('salesforceAPI/SforcePartnerClient.php');
require_once('salesforceAPI/SforceHeaderOptions.php');

// Login
$sfdc = new SforcePartnerClient();
$SoapClient = $sfdc->createConnection('salesforceAPI/wsdl.xml');
$loginResult = false;
$loginResult = $sfdc->login('user@domain.com', 'password' . 'securitytoken');

// Define constants for the web service. We'll use these later
$parsedURL = parse_url($sfdc->getLocation());
define ("_SFDC_SERVER_", substr($parsedURL['host'],0,strpos($parsedURL['host'], '.')));
define ("_WS_NAME_", 'MyWebService');
define ("_WS_WSDL_", _WS_NAME_ . '.xml');
define ("_WS_ENDPOINT_", 'https://' . _SFDC_SERVER_ . '.salesforce.com/services/wsdl/class/' . _WS_NAME_);
define ("_WS_NAMESPACE_", 'http://soap.sforce.com/schemas/class/' . _WS_NAME_);

Next we will call the web service. First thing to do is setup a Soap Client and modify the headers. Then we are setting up some fake data that maps to the expected inputs of the myMethod method in the web service. Then we actually call the web service.

// SOAP Client for Web Service
$client = new SoapClient(_WS_WSDL_);
$sforce_header = new SoapHeader(_WS_NAMESPACE_, "SessionHeader", array("sessionId" => $sfdc->getSessionId()));
$client->__setSoapHeaders(array($sforce_header));

// Setup fake data to send into the web service
$prodAmtArray = array();
	$prodAmtArray[] = array('productId'=>'01t60000000lvBN','amount'=>100);
	$prodAmtArray[] = array('productId'=>'01t60000000lvBS','amount'=>200);

$wrkArray = array(
				'contactId'=>'0036000000nVtpT',
				'inputs'=>$prodAmtArray
				);
				
// Call the web service
$response = $client->myMethod($wrkArray);

// Output results to browser
echo "<p><pre>" . print_r($response, true) . "</pre></p>";
echo "Contact Id is " . $response->result->contactId;

Results

Below displays what those 2 echo statements output.

stdClass Object
(
    [result] => stdClass Object
        (
            [contactId] => 0036000000nVtpTAAS
            [errorMessage] => No errors here.
            [inputs] => Array
                (
                    [0] => stdClass Object
                        (
                            [amount] => 100
                            [productId] => 01t60000000lvBNAAY
                        )
                    [1] => stdClass Object
                        (
                            [amount] => 200
                            [productId] => 01t60000000lvBSAAY
                        )
                )
            [success] => 1
        )
)
Contact Id is 0036000000nVtpTAAS
One last little trick

One annoyance during the development of Apex Web Services is having to continually generate a WSDL for the web service. When testing, I would find that I would continually need to put a new WSDL on the web server. I decided to automate this where I could add a variable to the queryString and have PHP refresh the WSDL on my web server. You need the cURL module for this, but most PHP installs have it.

$ch = curl_init();
	$fp = fopen(_WS_WSDL_, "w");
	curl_setopt($ch, CURLOPT_URL, _WS_ENDPOINT_);
	curl_setopt($ch, CURLOPT_FILE, $fp);
	curl_setopt($ch, CURLOPT_HEADER, 0);
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
	curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
	curl_setopt($ch, CURLOPT_COOKIE, 'sid='.$sfdc->getSessionId());
	setcookie("sid", $sfdc->getSessionId(), 0, "/", ".salesforce.com", 0);
	curl_setopt($ch, CURLOPT_TIMEOUT, 30);
	curl_exec($ch);
	fclose($fp);
curl_close($ch);

If you have experience with this kind of integration, please comment here on other tips & tricks.

Comments (23) comments feed

Campaign Member Summary using Google Charts

I was inspired by Sam Arjmandi’s post about embedding Google Charts into VisualForce pages.  I have a use case that requires a little different approach.  I needed to get a quick view of the Campaign Member Statuses for a Campaign.  I went ahead and started with Sam’s example and tweaked it for my purpose.  Here’s the result!

It’s an embedded VisualForce component in the Results section of my Campaign page.  It shows a quick count for each Member Status that is being used.  To get it going in your org, here’s what you need.

VisualForce Page

It’s a simple page containing 1 DIV so that I can set the background color to match that of a Page Layout.  Other than that it’s just an image returned from Google Charts.  There is a lot of flexibility with Google Charts.  Therefore, I made it so most of the URL can be tweaked in VisualForce. Only the data values and its labels come from the controller.  This is nice because you can edit VF in a production org, but you can’t edit Apex. This will let you change things like width, height, chart colors, chart type, etc.


<apex:page standardController="Campaign" extensions="VFController_CampaignMemberStatusChart">
    <style>
        #DIV_Container{padding: 0; margin: 0; background-color: #F3F3EC;}
    </style>
    <div id="DIV_Container">
        <!-- See http://code.google.com/apis/chart/ for more info on customizing the chart -->
        <apex:image url="http://chart.apis.google.com/chart?cht=p3&chs=1000x125&chf=bg,s,F3F3EC&chco=CC9933{!chartData}"></apex:image>
    </div>
</apex:page>

Apex Controller
The controller is an extension of the Campaign standard controller. This is so the VisualForce page becomes an option to include on a Campaign Page Layout. The method that does all the work is getChartData. It gets the available Campaign Member Statuses and then queries the Campaign Member object for that Campaign and adds a data/label value for each one. It then returns a query string that you include into the Image src on the Visual Force page.

I am bad at Test classes, but there is 1 in there that passes and should be good enough. This functionality is pretty harmless.

public class VFController_CampaignMemberStatusChart {

	private final Campaign camp;

	public VFController_CampaignMemberStatusChart(ApexPages.StandardController stdController) {
		this.camp = (Campaign)stdController.getRecord();
	}
	
	public String getChartData() {
		
		// The list of chart items	
		List<ChartDataItem> items = new List<ChartDataItem>();
		
		// List of valid Campaign Member Statuses for the Campaign
		List<CampaignMemberStatus> list_cms = [select Id, Label from CampaignMemberStatus where CampaignId = :camp.id];
		
		// Loop through each Campaign Member Status, get a count of Campaign Members and add it to the items list 
		for (CampaignMemberStatus cms:list_cms) {
			integer Count = [select count() from CampaignMember where CampaignId = :camp.id AND Status = :cms.Label];
			if (Count > 0) {
				items.add(new ChartDataItem(cms.Label, Count.format()));
			}
		}
		
		// Initialize Strings
		String chd = ''; // Data
		String chl = ''; // Labels
	
		for(ChartDataItem citem : items) {
			chd += citem.ItemValue + ',';
			chl += citem.Label + ' (' + citem.ItemValue + ')|';
		}
		
		//remove the last comma or pipe
		if (items.size() > 0) {
			chd = chd.substring(0, chd.length() -1);
			chl = chl.substring(0, chl.length() -1);
		}
		
		// We are only returning the values and labels. The rest of the URL string is in the VF page
		String result = '&chd=t:' + chd + '&chl=' + chl; // &chl returns with labels pointing to pie pieces
		//String result = '&chd=t:' + chd + '&chdl=' + chl; // &chdl returns with labels in a legend
		
		return result;
	}

	// Class holding each chart data item
	public class ChartDataItem {
		public String ItemValue {get; set;}
		public String Label {get; set;}
		
		public ChartDataItem(String Label, String Value)
		{
			this.Label = Label;
			this.ItemValue = Value;
		}
	}
	
	static testMethod void testVFController_Sidebar_Summary() {
		
		// Create Campaign
        Campaign c = new Campaign();
        c.Name = 'Test Campaign';
        insert c;
        
        // Create Lead
        Lead l = new Lead();
        l.LastName = 'Last Name';
        l.Company = 'Company';
        insert l;
        
        // Create Campaign Member
        CampaignMember cms = new CampaignMember();
        cms.CampaignId = c.id;
        cms.LeadId = l.id;
        insert cms;
		
		test.startTest();
		
		ApexPages.StandardController sc = new ApexPages.StandardController(c);
		VFController_CampaignMemberStatusChart controller = new VFController_CampaignMemberStatusChart(sc);
		String s1 = controller.getChartData();
		
		test.stopTest();
	}

}

Page Layout
When you add it to the Page Layout, make the height of the component the same as the height you specified in the VF page’s image src for Google Charts. In this example, it’s 125. Doing this will ensure the background colors match your Page Layout.

Comments (7) comments feed

Sidebar Summary using Visualforce

About a year ago, I posted about the Sidebar Summary.  The Sidebar Summary exists in the Salesforce.com sidebar and displays the counts of some important queries.  The counts are also hyperlinks to a view or report representing that query.  It’s a very handy thing to have in your sidebar and I use it all the time for my own work.  However, because it’s an s-Control, it runs a little slow.  In fact, it ran slow enough to make me uncheck the user interface option “Show Custom Sidebar Components on All Pages”.

I changed it into a Visualforce page with a custom Apex controller and now it runs super fast and I am able to keep the “Show Custom Sidebar Components on All Pages” option turned on and see it on every page I go to.  There’s a bit of hardcoding in here, but it gets the job done pretty well.  Bye bye s-Control.

Visualforce

The Page is almost all raw HTML.  The only dynamic thing in there are the count values.  Each one retrieves the value from a specific “get” method in the controller.  If you like the queries I use, then the only thing you’ll need to confirm are the URLs that get linked to.  The first 2 go to Views in my Org and the last 2 go to Reports in my Org.  You’ll need to change those URLs.

I named the VF Page “SidebarSummary”.


<apex:page controller="VFController_Sidebar_Summary" sidebar="false" showHeader="false" standardStylesheets="true">
<style type="text/css" media="all">
body{margin: 0; padding: 0; color: #000000; background-color: #E8E8E8;}
#DIV_Container {background-color: #F3F3EC;}
</style>
<div id="DIV_Container">
<table>
<tr><td><em>Unread Leads</em>:  </td><td><a href="/00Q?fcf=00B30000005JhsT" target="_parent"><b>{!UnreadLeads}</b></a></td></tr>
<tr><td><em>Leads - Not Contacted</em>:  </td><td><a href="/00Q?fcf=00B30000005Jhru" target="_parent"><b>{!NotContactedLeads}</b></a></td></tr>
<tr><td><em>Oppty - Next 30 Days</em>:  </td><td><a href="/00O30000001aEHV" target="_parent"><b>{!Next30DayOppty}</b></a></td></tr>
<tr><td><em>Oppty - Past Due</em>:  </td><td><a href="/00O30000001aEHV" target="_parent"><b>{!PastDueOppty}</b></a></td></tr>
</table>
</div>
</apex:page>

Apex

The controller has a method for each query to be run.  Each query is a count() query and returns an Integer.  At the end is a really lame Test method, but it does get 100% of the code covered.  I am certain the code works, so I didn’t do too much with the Test method.  Salesforce just requires the code to be tested.


public class VFController_Sidebar_Summary {

public Integer getUnreadLeads() {
return [
select count() from Lead
where IsConverted = False
AND IsUnreadByOwner = TRUE
];
}

public Integer getNotContactedLeads() {
return [
select count() from Lead
where IsConverted = False
AND Status = 'Open - Not Contacted'
];
}

public Integer getNext30DayOppty() {
return [
select count() from Opportunity
where IsClosed = False
AND (CloseDate = Next_N_DAYS:30 OR CloseDate = TODAY)
];
}

public Integer getPastDueOppty() {
return [
select count() from Opportunity
where IsClosed = False
AND CloseDate < TODAY
&#93;;
}

static testMethod void testVFController_Sidebar_Summary() {
Test.setCurrentPageReference(new PageReference('Page.SidebarSummary'));
VFController_Sidebar_Summary controller = new VFController_Sidebar_Summary();
Integer i1 = controller.getUnreadLeads();
Integer i2 = controller.getNotContactedLeads();
Integer i3 = controller.getNext30DayOppty();
Integer i4 = controller.getPastDueOppty();
}

}

&#91;/sourcecode&#93;

<span style="text-decoration: underline;"><strong>Homepage HTML Component</strong></span>

I created a component for the Narrow side and put the following HTML into the editor.  Essentially, you create an IFRAME and embed the VF page into it.  I found a (unsupported) trick on the forums to remove the developer bar from a page.  Just add <strong>?core.apexpages.devmode.url=1</strong> to the URL.  This will turn off development mode when that page is rendered.  This is important for this little iFrame page on the sidebar.  From what I've gathered, this hack is not supported and could change at any time.

The code below should work for you.  The only thing you might need to change is the Page URL if you didn't name your page SidebarSummary and the height of it.



<iframe src="/apex/SidebarSummary?core.apexpages.devmode.url=1" frameborder="0" height="100" width="100%"></iframe>

Let me know what you think.

Comments (24) comments feed

Bulkifying a Trigger (an example)

Back in June, I blogged about a trigger I made to auto-create a Campaign Member record for new Leads if the Lead Source value matched the Name of an existing Campaign.  Steve Andersen rightfully commented on that post about the need to bulkify the trigger.  I have been pretty slow in learning Apex and over the past week or so things have started clicking for me.  I am now (finally) beginning to “get it”.  With the help of Matt Kaufman and a friend at Salesforce, I have come to understand Apex a bit more and can much more easily apply it to my day to day Salesforce work.

Therefore, I thought I should correct the trigger I wrote and make it bulkified.  This should serve as a good before and after example of a trigger that accomplishes the same thing, but is now bulkified.

The original Trigger
Below is how the trigger looked originally.

trigger Create_CampaignMember_For_New_Leads on Lead (after insert) {

	try {	
		
		if (Trigger.new.size() == 1) {
			
			List <CampaignMember> cm = new list<CampaignMember>();
			
			for(Lead L : Trigger.new) {
				
					String cname = L.leadsource;
					
					// Added for AppExchange Partners that get leads via AppExchange where Salesforce added the "dup-" term to signify a duplicate
					String replaceText2 = 'dup-';
					cname = cname.replace(replaceText2,'');
					
					List <Campaign> c = [select id, name from Campaign where name = :cname limit 1];
					
					if(!c.isEmpty()){
						CampaignMember cml = new CampaignMember();
						cml.campaignid = c[0].id;
						cml.leadid = l.id;
						cm.add(cml);
					}
			}
			
			if(!cm.isEmpty()){
				insert cm;
			}
		}
		
		
	} catch(Exception e) {
		system.debug ('error: ' + e.getMessage() );
	} 
}

Bulkified Trigger
And now a corrected version to be bulkified.

trigger Create_CampaignMember_For_New_Leads on Lead (after insert) {

	/*
    * Loop through all leads and collect the necessary lists
    */
	list<Lead> theLeads = new list<Lead>(); // List containing each Lead being processed
	list<String> cNames = new list<String>(); // List of Campaign Names
	map<String, String> map_lSource_to_cName = new map<String, String>(); // Mapping Lead Sources to Campaign Names. We have this because we are cleaning up Lead Sources in some cases. 
	String wrkText = ''; // Temporary, working variable
	String replaceText = 'dup-'; // Text to replace. This is included for ISV partners who want to remove the "-dup" string that is included for duplicate AppExchange Lead Submissions  
	
		for(Lead l:trigger.new) { 
			theLeads.add(l); // add lead to the main lead list
			if (l.leadsource != null) {
				wrkText = l.leadsource;
				wrkText = wrkText.replace(replaceText,'');
				cNames.add(wrkText); // add to list of Campaign Names
				map_lSource_to_cName.put(l.leadsource,wrkText); // add to map of Lead Sources to Campaign Names
			}
		}
	
	/*
	* Create a map containing an association of Campaign Names to Campaign IDs
	*/
	list<Campaign> theCampaigns = [SELECT Id, Name FROM Campaign WHERE Name IN :cNames]; // Campaign sObjects we are dealing with
	map<String, ID> map_cName_to_cID = new map<String, ID>(); // Mapping Campaign Names to Campaign IDs
	
		for (Campaign c:theCampaigns) {
			map_cName_to_cID.put(c.Name,c.Id);
		}
		
	/*
	* Loop through the main list of Leads
	*/
	list <CampaignMember> theCampaignMembers = new list<CampaignMember>(); // List containing Campaign Member records to be inserted
	
	for (Lead l:theLeads) {
		if(map_cName_to_cID.get(map_lSource_to_cName.get(l.leadsource)) != null) {
			CampaignMember cml = new CampaignMember();
			cml.leadid = l.id;
			cml.campaignid = map_cName_to_cID.get(map_lSource_to_cName.get(l.leadsource));
			theCampaignMembers.add(cml);
		}

	}
	
	/*
	* Insert the list of Campaign Members
	*/
	if(!theCampaignMembers.isEmpty()){
		insert theCampaignMembers;
	}

}

Updated Test Class (100% code coverage)
I updated the test class too to make it cover 100% of the code.

public class Create_CampaignMember_For_New_Leads {

	static testMethod void Create_CampaignMember_For_New_Leads() {
	
		// Create a Campaign to be matched on
		Campaign C1 = new Campaign();
		C1.Name = 'Matching Campaign';
		insert C1;
		
		// Create a Lead with a Lead Source matching a Campaign
		Lead L1 = new Lead();
		L1.lastname = 'Create_CampaignMember_For_New_Leads';
		L1.firstname = 'Test For';
		L1.company = 'Company ABC';
		L1.leadsource = 'Matching Campaign';
		   
		insert L1;
		String holder = L1.id;
		
		List <CampaignMember> cm = [select id from CampaignMember where leadid = :holder limit 1];
		system.AssertEquals(1,cm.size());
		
		// Create a Lead without a Lead Source matching a Campaign
		Lead L2 = new Lead();
		L2.lastname = 'Create_CampaignMember_For_New_Leads';
		L2.firstname = 'Test For';
		L2.company = 'Company ABC';
		L2.leadsource = 'No Matching Campaign';
		   
		insert L2;
		
		String holder2 = L2.id;
		
		List <CampaignMember> cm2 = [select id from CampaignMember where leadid = :holder2 limit 1];
		system.AssertEquals(0,cm2.size());
	                       
	}
	
}

Feel free to make additional recommendations on this code. I updated the Arrowpointe Google Code Site with this code too.

Comments (16) comments feed

Make your Mass Email Activities more meaningful

It had always bothered me that activities generated from Mass Emails only had a subject of “Mass Email:” and nothing in the body.  I couldn’t tell what I mass emailed to someone!

Deep inside the forums, I found the trick to get some meaning into those activities.  The best you can do is get the Subject filled in, but that’s something.  The subject for a Mass Email = “Mass Email: Email Template Description“.  Giving your email templates a description is the only way to get a meaningful mass email activity.  The Description is an often overlooked field on Email Template because it’s not required, so go make sure you have it filled in.

Once you do this, the next Mass Email you send can start having some meaning.

I created an idea on Idea Exchange for what I think a better design would be.  Vote on it and comment there to improve the recommendation.

Comments off comments feed

Next entries » · « Previous entries