<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Jeremy Cook &#187; Web Services</title>
	<atom:link href="http://jeremycook.ca/category/web-development/web-services/feed/" rel="self" type="application/rss+xml" />
	<link>http://jeremycook.ca</link>
	<description>Random musings on web development and PHP</description>
	<lastBuildDate>Mon, 30 Jan 2012 02:31:54 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.3.1</generator>
		<item>
		<title>Using the Countable Interface</title>
		<link>http://jeremycook.ca/2012/01/01/using-the-countable-interface/</link>
		<comments>http://jeremycook.ca/2012/01/01/using-the-countable-interface/#comments</comments>
		<pubDate>Sun, 01 Jan 2012 17:14:43 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Services]]></category>
		<category><![CDATA[SPL]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=230</guid>
		<description><![CDATA[PHP provides a number of predefined interfaces and classes that can really make your life as a developer easier but which are often overlooked. The functionality offered by the Standard PHP Library (SPL) and the predefined interfaces is extremely cool and very powerful but very underutilised. I&#8217;ve found myself reaching more and more for these features recently and [...]]]></description>
			<content:encoded><![CDATA[<p>PHP provides a number of predefined interfaces and classes that can really make your life as a developer easier but which are often overlooked. The functionality offered by the <a href="http://ca2.php.net/manual/en/book.spl.php">Standard PHP Library</a> (SPL) and the <a href="http://ca2.php.net/manual/en/reserved.interfaces.php">predefined interfaces</a> is extremely cool and very powerful but very underutilised. I&#8217;ve found myself reaching more and more for these features recently and it set me to thinking why are they not more widely used? After a little bit of thought I think I&#8217;ve worked it out. For the most part PHP extensions allow you to achieve some concrete action which is easy to grasp. In comparison it&#8217;s much harder to see what the SPL and predefined interfaces are going to do for you. While they let you hook into the core PHP engine in really powerful and useful ways it&#8217;s not easy to see at first glance how to use them. The problem is in a lack of &#8216;real-world&#8217; examples. While most of this functionality is well documented the examples are often somewhat dry and academic. This isn&#8217;t really the fault of the PHP manual: the functionality in the SPL or predefined interfaces can be applied to so many different ways that it&#8217;s hard to provide concrete uses for them. I thought I&#8217;d write a few articles with examples of how I&#8217;ve used these classes and interfaces in the hope that someone would find it useful. I&#8217;d love it if people felt like commenting with their own examples too. I&#8217;ll start with a quick look at the Countable interface.</p>
<h2>The Countable Interface</h2>
<p>The <a href="http://ca2.php.net/manual/en/class.countable.php">Countable interface</a> at it&#8217;s simplest level allows you to pass objects that implement it to PHP&#8217;s native count function. The interface is, on the surface, extremely easy to implement. You only need to add the Countable::count() method to implement the interface in your class and this method only needs to return an integer. If you pass an object that implements Countable to the PHP count function the PHP interpreter will automatically call the count method in the object, returning the value from it to you to use.</p>
<h2>Why is Countable so Useful?</h2>
<p>Like all of the predefined interfaces and SPL functionality the real power of countable comes from the ability to effectively override and re-implement core PHP functionality with your own logic. I&#8217;ll give three brief examples here of ways that I&#8217;ve personally used Countable recently.</p>
<h3>Accessing the State of a Private or Protected Property</h3>
<p>One of the simplest uses of countable is to return the count of an array that&#8217;s held as a protected or private property of an object. Take the following example:</p>
<pre class="brush: php; title: ; notranslate">

class SomeClass implements Countable {

 protected $_data = array();

 public function doSomething()
 {
//Code to change $_data here
 }

 public function count()
 {
return count($this-&gt;_data);
 }
}
</pre>
<p>In this example some data is stored in the object as an array and the Countable:Count method simply returns the count of that array. While this is obviously a very simple example it can be very powerful. I&#8217;ve been doing a lot of work with a web service in the last few weeks and have used code very much like this. After a web service call the resulting XML is processed using XPath and the resulting array is stored in a protected property. The count method would then return the number of results of the web services call. Using count in this way obviously makes a lot of sense when coupled with implementing Iterator or ArrayAccess to allow access to the data held in the protected property.</p>
<h3>Using Countable to Signal the State of an Object</h3>
<p>Countable can also be used to signal the state of an object. Imagine the above code, amended slightly to this:</p>
<pre class="brush: php; title: ; notranslate">

class SomeOtherClass implements Countable {

protected $_data;

public function doSomething()
{
//Code to change $_data here
}

public function count()
{
return is_null($this-&gt;_data) ? 0 : 1;
}
}
</pre>
<p>In this case count will only ever return 0 or 1, depending on whether the protected property $_data has a value set in it or not. Using this code could look something like this:</p>
<pre class="brush: php; title: ; notranslate">

$obj = new SomeOtherClass();

$obj-&gt;doSomething();

if (count($obj)) {

//$_data is not null, proceed accordingly.

}
</pre>
<p>I&#8217;ve also used this recently with a web service call. In this case the web service call was expected to return a single value, which is stored in $_data. Implementing countable like this simply signifies to the calling code if a value has been successfully retrieved or not.</p>
<h3>Using Countable with a Database Table</h3>
<p>Another interesting use of Countable is with a class that maps to a database table in some way. Imagine a case where you have a database table where values are only considered &#8216;live&#8217; if some condition is met. Implementing countable on an object that encapsulates the db table can then be used to flag if the table contains any &#8216;live&#8217; values. The following example uses Zend_Db_Table:</p>
<pre class="brush: php; title: ; notranslate">

class AnotherClass extends Zend_Db_Table implements Countable {

 public function count()
 {
$select = $this-&gt;select(Zend_Db_Table::SELECT_WITH_FROM_PART)
-&gt;from('some_table', array('Count' =&gt; 'COUNT(*)'))
-&gt;where('SomeCondition = SomeValue');
$row = $this-&gt;fetchRow($select);
return (int) $row-&gt;Count;
 }
}
</pre>
<p>In your calling code you would then simply do this:</p>
<pre class="brush: php; title: ; notranslate">

$obj = new AnotherClass();

if (count($obj)) {

//Table has 'live' values. Do something here

}
</pre>
<p>This can be a very useful way of working with a database table.</p>
<h2>A Word of Caution&#8230;</h2>
<p>The only limit to the way Countable can be used is the imagination of the developer, and this is also a danger. All developers are familiar with using the count() function to get hold of the number of values in an array. When implementing Countable the further you move the count() method away from this functionality the less intuitive the code becomes and the greater the chance that the developer using it might make a mistake. This is true of all of the predefined interfaces however. While they provide great power the further away from core PHP functionality you get while implementing them in your own code the less intuitive they become. However, don&#8217;t let this stop you! The next time you go to write a method called something like &#8216;getItemCount()&#8217; in a class don&#8217;t do it! Implement the Countable interface instead and start hooking into the power of the core PHP engine.</p>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2012/01/01/using-the-countable-interface/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>Using SalesNet Web Services from PHP</title>
		<link>http://jeremycook.ca/2011/12/06/using-salesnet-web-services-from-php/</link>
		<comments>http://jeremycook.ca/2011/12/06/using-salesnet-web-services-from-php/#comments</comments>
		<pubDate>Wed, 07 Dec 2011 02:05:56 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Services]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=223</guid>
		<description><![CDATA[I&#8217;ve been working on some projects recently where I&#8217;ve had to make heavy use of the SalesNet CRM API. I&#8217;ve written some code that makes basic access of the API easier and I thought I would write about it a little here. The SalesNet API is vast in scope (the documentation runs to 144 pages!) [...]]]></description>
			<content:encoded><![CDATA[<p>I&#8217;ve been working on some projects recently where I&#8217;ve had to make heavy use of the <a href="http://www.salesnet.com/">SalesNet CRM</a> API. I&#8217;ve written some code that makes basic access of the API easier and I thought I would write about it a little here. The SalesNet API is vast in scope (the documentation runs to 144 pages!) and very fully featured. However, it is not particularly easy to use at all and is somewhat frustrating. My aim in the code was to ease some of the basic frustrations in using the API. Full code and unit tests for the classes that I wrote can be found on <a href="https://github.com/JCook21/SalesNet">GitHub</a>.</p>
<h2>API Format</h2>
<p>The SalesNet API is SOAP based and consists of 8 separate endpoints, each of which allows you to operate on different parts of a companies data such as deals, accounts, contacts, etc. Before you can use any of these services it&#8217;s necessary to authenticate against a separate security endpoint. This returns a token which is valid for between 1-12 hours (depending on the account setup in SalesNet). This token must then be set in a SOAP header for use in requests to any of the main SalesNet endpoints. Each of the API methods accepts many arguments to control precisely the data that is returned and the only thing I can suggest to anyone looking to use the API is to refer to the documentation. One of the quirks of the API is that data is returned as XML, embedded in a stdClass object. For example, a call to the GetDeals method of the deals endpoint returns a stdClass object (say $obj), and to access the response data the following would be needed:</p>
<pre class="brush: php; title: ; notranslate">

$response = $obj-&gt;GetDealsResult-&gt;any;
</pre>
<p>In the above example $response would now include the XML data &#8216;payload&#8217;, which can then be parsed. It&#8217;s often not that easy to parse the XML returned as it seems to be an XML representation of a Microsoft ADODB recordset, which in some cases is not even properly formed XML. I&#8217;ve had luck using SimpleXML and Xpath to extract the nodes I&#8217;m after however.</p>
<h2>The Code</h2>
<p>To work with the various SalesNet API&#8217;s I created two classes, one of which handles authentication and the other of which acts as a proxy to catch and pass SOAP calls through to the API. Both of the classes make use of &#8216;lazy loading&#8217; and will only connect to SalesNet as and when a method is called that requires a SOAP call.</p>
<h3>The Authentication Class</h3>
<p>My goal in the authentication class was to simplify authenticating against the SalesNet security endpoint. The object accepts a company name, username and password and will retrieve a security token from SalesNet. This is then set in a PHP SoapHeader object and is returned for use whenever the doLogin() method is called. Internally the class will keep track of when the token expires and will automatically fetch a new token from SalesNet as and when required. My thought is that an object of this class can be serialized and cached to be stored and shared between many requests as a security token can be valid for up to 12 hours.</p>
<h3>The SoapProxy Class</h3>
<p>The SoapProxy class provides a proxy to trap calls to the many SalesNet API methods. Since there are so many methods cross so many endpoints it would be impossible to map these out to individual methods so I chose to use __call() instead to pass calls through to a PHP SoapClient object. The class stores the URL endpoints of the various services as constants and an API endpoint can be set when an object is instantiated or before any method is called. The object also accepts an instance of the Authentication class and uses this internally to set the security SoapHeader for API calls.</p>
<h2>A Brief Example</h2>
<p>So, enough talk! As I said earlier full code can be found on <a href="https://github.com/JCook21/SalesNet">GitHub</a> but here&#8217;s a brief example of how to use the classes to call the GetDeals method of the Deals endpoint.</p>
<pre class="brush: php; title: ; notranslate">
&lt;?php
use WebServices\Soap\SalesNet\Authentication, WebServices\Soap\SalesNet\SoapProxy;

try {
    $auth = new Authentication();
    $auth-&gt;setCompanyLogin('COMPANY LOGIN HERE')
         -&gt;setUserName('USERNAME HERE')
         -&gt;setPassword('PASSWORD HERE');

    $soap = new SoapProxy(SoapProxy::DEALS, $auth);
    $args = array(
        //Set up method arguments here
    );
    $response = $soap-&gt;GetDeals($args);
} catch (BadMethodCallException $e) {
    /**
     * Errors will be caught here.
     * The BadMethodCallException can have a previous exception of a SoapFault if there is one.
     */
}
</pre>
<p>That&#8217;s all there is to it. To use other endpoints simply change the constant passed to the SoapProxy constructor and call another Soap method, with the correct arguments of course. Let me know of any comments on this code or improvements people can see. Enhancements and fixes on GitHub are welcome.</p>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2011/12/06/using-salesnet-web-services-from-php/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Google Custom Search Engine</title>
		<link>http://jeremycook.ca/2010/06/27/google-custom-search-engine/</link>
		<comments>http://jeremycook.ca/2010/06/27/google-custom-search-engine/#comments</comments>
		<pubDate>Mon, 28 Jun 2010 00:29:39 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Search]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Services]]></category>
		<category><![CDATA[Google]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=124</guid>
		<description><![CDATA[I recently published my first class on PHPClasses and thought I would write a brief post about it here. The class allows a developer to send a query to a Google Site Search custom search engine, using the XML API. Google Site Search Google Site Search can be used to create a customised search engine [...]]]></description>
			<content:encoded><![CDATA[<p>I recently published my first class on <a href="http://www.phpclasses.org/">PHPClasses</a> and thought I would write a brief post about it here. The class allows a developer to send a query to a <a href="http://www.google.com/sitesearch/">Google Site Search</a> custom search engine, using the XML API.</p>
<h2>Google Site Search</h2>
<p>Google Site Search can be used to create a customised search engine for your website. The costs start at $100 per year (at the time of writing) for a website of up to 1,000 pages and this allows 250,000 searches per year. The search engine can be queried in a number of ways, one of which is via a REST web service. The service seems to be ideal for small to medium websites. Larger websites would probably benefit from using a service such as Solr or Lucene to create a search index on the sites&#8217; web server, which may well provide a more customisable solution. For sites where it&#8217;s not possible to create such an index, or the owner doesn&#8217;t want to go to the effort, Google Site Search seems to be ideal.</p>
<h2>The Google Custom Search Class</h2>
<p>The class I&#8217;ve written can be downloaded from <a href="http://www.phpclasses.org/package/6301-PHP-Perform-searches-the-Google-Custom-Search-API.html">PHPClasses</a> along with a file giving some examples of it&#8217;s use. The class can use cURL or the PHP HTTP stream wrapper to perform the web service request, with the constructor deciding on which method to use based on what the server has installed. The response from Google, consisting of search results and optional spelling suggestions, is processed using SimpleXML into arrays which are set as properties of the object. The search results XML contains the results as HTML and the class allows a user to specify how this should be handled. The user can ask for the results to be made available as plain text (no HTML and no entities encoded), plain text with all HTML special characters entity encoded, or with the HTML that Google returns left in. The user can specify which character encoding, defaulting to ISO-8859-1, a search query uses and results are returned in the same encoding. The Google API also allows many parameters to be set in the request to  the service (see <a href="http://www.google.com/cse/docs/resultsxml.html#wsRequestParameters">http://www.google.com/cse/docs/resultsxml.html#wsRequestParameters</a> for full documentation). The class allows an array of key =&gt; value  pairs to be passed which are then set as query string parameters,  allowing the search to be further customised. Finally, any errors generated in the process cause a RuntimeException to be thrown which has to be caught outside of the object. Below are some examples of using the class.</p>
<pre class="brush: php; title: ; notranslate">

try {
		//A simple example, default parameters in constructor and no extra parameters set.
		$search = new GoogleCustomSearch('CSE Key Here');
		$search-&gt;query('some search text');
		//The results property contains the search results, spellingSuggestions contains any spelling suggestions from Google.
		var_dump($search-&gt;results);
		var_dump($search-&gt;spellingSuggestions);

		//Example with results returned with HTML from Google left in and character encoding set to UTF-16. Result text will also be encoded in UTF-16
		//NOTE: When using a custom character encoding the search query must also be encoded using this character set.
		//The object defaults to character encoding in iso-8859-1 and assumes that queries passed to it are also encoded in this character set.
		$search = new GoogleCustomSearch('CSE Key Here', 'utf-16', GoogleCustomSearch::HTML);
		$search-&gt;query('some search text');
		var_dump($search-&gt;results);
		var_dump($search-&gt;spellingSuggestions);

		//Use the same object to perform an extra search, but with some different parameters
		$search-&gt;charEncoding = 'iso-8859-1';
		$search-&gt;processText = GoogleCustomSearch::ENTITIES_ENCODED; //Strips HTML but leaves HTML special characters entity encoded.
		$search-&gt;query('another query');
		var_dump($search-&gt;results);
		var_dump($search-&gt;spellingSuggestions);

		//Example using an array to set extra, custom options in the query string used to send a request to Google.
		//See http://www.google.com/cse/docs/resultsxml.html#WebSearch_Query_Parameter_Definitions for a full list of query parameter options that may be passed in the request.
		$opts = array(
			'lr' =&gt; 'lang_fr', //Request search results only for French language pages.
			'cr' =&gt; 'countryCA', //Request search results only for a particular country, in this case Canada
			'num' =&gt; 7 //Limit the search to a maximum of 7 results
		);
		//Create an object using default search options
		$search = new GoogleCustomSearch('CSE Key Here');
		//Set the custom options for the search.
		//These can also be set by passing the $opts array as the final argument in the object constructor.
		$search-&gt;opts = $opts;
		$search-&gt;query('yet another query');
		var_dump($search-&gt;results);
		var_dump($search-&gt;spellingSuggestions);
	}
	catch (RuntimeException $e) {
		//Handle any exceptions raised by the object here.
		echo $e-&gt;getMessage();
	}
</pre>
<p>I hope somebody finds the class to be useful. It&#8217;s still a version 1 release and I can see two major ways that it could be improved. Firstly, the handling of error response from the API is a little basic. I need to do some more research into error response codes that the API generates and use that to make the error messages set in any RuntimeException a little more specific. Secondly the response XML contains much more information than I am currently processing. I could make much more of it available as properties of the object and/or make the XML returned from the service available as a property of the object to allow a user to process it in any way they wish. If anyone has any problems, comments or suggestions post them below and I&#8217;ll get back to you.</p>
<div id="_mcePaste" style="position: absolute; left: -10000px; top: 0px; width: 1px; height: 1px; overflow: hidden;">try {<br />
//A simple example, default parameters in constructor and no extra parameters set.<br />
$search = new GoogleCustomSearch(&#8216;CSE Key Here&#8217;);<br />
$search-&gt;query(&#8216;some search text&#8217;);<br />
//The results property contains the search results, spellingSuggestions contains any spelling suggestions from Google.<br />
var_dump($search-&gt;results);<br />
var_dump($search-&gt;spellingSuggestions);</p>
<p>//Example with results returned with HTML from Google left in and character encoding set to UTF-16. Result text will also be encoded in UTF-16<br />
//NOTE: When using a custom character encoding the search query must also be encoded using this character set.<br />
//The object defaults to character encoding in iso-8859-1 and assumes that queries passed to it are also encoded in this character set.<br />
$search = new GoogleCustomSearch(&#8216;CSE Key Here&#8217;, &#8216;utf-16&#8242;, GoogleCustomSearch::HTML);<br />
$search-&gt;query(&#8216;some search text&#8217;);<br />
var_dump($search-&gt;results);<br />
var_dump($search-&gt;spellingSuggestions);</p>
<p>//Use the same object to perform an extra search, but with some different parameters<br />
$search-&gt;charEncoding = &#8216;iso-8859-1&#8242;;<br />
$search-&gt;processText = GoogleCustomSearch::ENTITIES_ENCODED; //Strips HTML but leaves HTML special characters entity encoded.<br />
$search-&gt;query(&#8216;another query&#8217;);<br />
var_dump($search-&gt;results);<br />
var_dump($search-&gt;spellingSuggestions);</p>
<p>//Example using an array to set extra, custom options in the query string used to send a request to Google.<br />
//See http://www.google.com/cse/docs/resultsxml.html#WebSearch_Query_Parameter_Definitions for a full list of query parameter options that may be passed in the request.<br />
$opts = array(<br />
&#8216;lr&#8217; =&gt; &#8216;lang_fr&#8217;, //Request search results only for French language pages.<br />
&#8216;cr&#8217; =&gt; &#8216;countryCA&#8217;, //Request search results only for a particular country, in this case Canada<br />
&#8216;num&#8217; =&gt; 7 //Limit the search to a maximum of 7 results<br />
);<br />
//Create an object using default search options<br />
$search = new GoogleCustomSearch(&#8216;CSE Key Here&#8217;);<br />
//Set the custom options for the search.<br />
//These can also be set by passing the $opts array as the final argument in the object constructor.<br />
$search-&gt;opts = $opts;<br />
$search-&gt;query(&#8216;yet another query&#8217;);<br />
var_dump($search-&gt;results);<br />
var_dump($search-&gt;spellingSuggestions);<br />
}<br />
catch (RuntimeException $e) {<br />
//Handle any exceptions raised by the object here.<br />
echo $e-&gt;getMessage();<br />
}</p>
</div>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2010/06/27/google-custom-search-engine/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Creating Professional Documents the Easy Way</title>
		<link>http://jeremycook.ca/2010/05/16/creating-professional-documents-the-easy-way/</link>
		<comments>http://jeremycook.ca/2010/05/16/creating-professional-documents-the-easy-way/#comments</comments>
		<pubDate>Mon, 17 May 2010 02:46:58 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Web Services]]></category>
		<category><![CDATA[Zend Framework]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=88</guid>
		<description><![CDATA[Have you ever tried to create a professional quality document programmatically from within PHP? It&#8217;s a real pain and surprisingly tricky to do. There are limited options for PHP developers to produce files in the formats most often used in business. There are libraries for producing PDF files in PHP but they are complex and [...]]]></description>
			<content:encoded><![CDATA[<p>Have you ever tried to create a professional quality document programmatically from within PHP? It&#8217;s a real pain and surprisingly tricky to do. There are limited options for PHP developers to produce files in the formats most often used in business. There are libraries for producing PDF files in PHP but they are complex and are difficult to use for producing documents. The options to create Microsoft Word files seem to be even more limited. It is possible to create RTF files from PHP but as these are not the most commonly used format in business the user will have to convert the file to another format before saving it. I&#8217;m working on a project where one of the requirements is for a user to download a a professional quality document with variable content, which is determined by their answers to a questionnaire. I was looking at having to create RTF files before I stumbled on a great service offered by <a href="http://www.livedocx.com/">Live Docx</a> that seems to offer the holy grail of programmatically creating professional quality documents with minimum hassle.</p>
<h2>What is LiveDocx?</h2>
<p>The Live Docx service allows a programmer to take a document template and through a SOAP based web service populate it with content. The resulting document can then be retrieved to the web server in one of many common formats including PDF, MS Word .doc and .docx and RTF. Best of all the service is completely free at the lowest level, but depending on the requirements of your application it may be more appropriate to use one of the paid for levels of service. The templates can be created in .doc, .docx, .rtf and .txd formats and use mail merge to populate the content. The great advantage is that you can use all of the features of a word processing program to design your document, ending up with a fully professional quality document. Best of all you don&#8217;t even need to write any code to access the web service. The latest release of the Zend Framework (1.10) includes a component, Zend_Service_LiveDocx, to access and work with the service and there&#8217;s also a .NET library to download for Microsoft developers.</p>
<h2>Creating Templates</h2>
<p>As mentioned earlier the LiveDocx service uses mail merge fields to perform its magic. You create a document that contains one or more merge fields and then assign content to these fields programmatically at run time. The LiveDocx servers replace the mail merge fields with the content in the same way as a word processing program would, making the completed document available to you. Templates can either be stored directly on the LiveDocx servers or on your own webserver. The SOAP service allows to you specify the name of the template you want to use and whether it is stored locally or remotely. Which one is  chosen will largely depend on the application but storing templates on your own web server does mean that the template must be sent to LiveDocx along with the content before the completed document can be retrieved. This obviously significantly increases the amount of information that needs to be transmitted over the internet but the demands of your application may dictate that templates be stored locally.</p>
<h2>Using Zend_Service_LiveDocx</h2>
<p>The Zend Framework LiveDocx component is very easy to use and complete documentation can be found <a href="http://framework.zend.com/manual/en/zend.service.livedocx.html">here</a>. The Zend Framework download also includes several examples of using the service. Basically you instantiate a Zend_Service_LiveDocx_MailMerge object, setting your username, password and the template you wish to use, before assigning content to the object in much the same way that you would with Smarty. You then call a createDocument() method to produce the document on the remote server followed by a retrieveDocument() method to get the document produced back as a string. The latter method takes an argument which specifies the type of file that should be created. That&#8217;s basically it. The code below is a simple view class that I wrote for my application that uses the Zend_Service_LiveDocx to produce a document.</p>
<pre class="brush: php; title: ; notranslate">

&lt;?php
 /**
 * View for the download plan functionality. Uses the LiveDocx web service and the ZendFramework LiveDocx SOAP integration to produce a file.
 * @author Jeremy Cook
 * @version 1.0
 * @see http://framework.zend.com/manual/en/zend.service.livedocx.html
 */
 class planView {
 /**
 * Array of content to be used in the plan
 *
 * @var array
 */
 protected $content = array();
 /**
 * The type of file to be requested from LiveDocx
 *
 * @var string
 */
 protected $fileType;
 /**
 * Name of the template on LiveDocx to use
 *
 * @var string
 */
 protected $template;
 /**
 * Username for LiveDocx
 *
 * @var string
 */
 protected $username;
 /**
 * Password for LiveDocx
 *
 * @var string
 */
 protected $password;
 /**
 * Name to give the downloaded file
 *
 * @var string
 */
 protected $filename;
 /**
 * Constructor
 *
 * @param array $content
 * @param string $fileType Type of file to be requested from LiveDocx
 * @param string $template Name of the remote template to use on LiveDocx
 * @param string $filename Name to use for the file produced by LiveDocx
 * @param string $username
 * @param string $password
 * @return planView
 */
 public function __construct (array $content, $fileType, $template, $filename, $username, $password) {
    $this-&gt;content = $content;
    $this-&gt;fileType = $fileType;
    $this-&gt;template = $template;
    $this-&gt;filename = $filename;
    $this-&gt;username = $username;
    $this-&gt;password = $password;
 }
 /**
 * Method to produce the plan document
 *
 * @throws Zend_Service_LiveDocx_Exception
 * @throws RuntimeException
 */
 public function getPlan () {
    require_once 'Zend/Service/LiveDocx/MailMerge.php';
    $doc = new Zend_Service_LiveDocx_MailMerge();
    $doc-&gt;setUsername($this-&gt;username)-&gt;setPassword($this-&gt;password);
    $doc-&gt;setRemoteTemplate($this-&gt;template);
    foreach ($this-&gt;content as $key =&gt; $value) {
       $doc-&gt;assign($key, $value);
    }
    $doc-&gt;createDocument();
    $document = $doc-&gt;retrieveDocument($this-&gt;fileType);
    unset($doc);
    $output = fopen('php://output', 'w');
    if (!$output)
       throw new RuntimeException('Unable to bind PHP output as a file handle');
    switch ($this-&gt;fileType) {
       case 'pdf':
          header ('Content-Type: application/pdf');
          break;
       case 'doc':
       case 'docx':
          header ('Content-Type: application/vnd.ms-word');
          break;
    }
    $filename = str_replace(' ', '_', $this-&gt;filename) . &quot;.{$this-&gt;fileType}&quot;;
    header (&quot;Content-Disposition: attachment; filename=\&quot;$filename\&quot;&quot;);

    //Make sure the browser doesn't cache the file
    //Set headers to ensure the document is not cached.
    header (&quot;Expires: Mon, 26 Jul 1990 05:00:00 GMT&quot;);
    header (&quot;Last-Modified: &quot; . gmdate(&quot;D, d M Y H:i:s&quot;) . &quot; GMT&quot;);
    header (&quot;Cache-Control: no-cache, must-revalidate&quot;);
    header (&quot;Pragma: no-cache&quot;);
    //Write the document to php output
    fwrite($output, $document);
    fclose($output);
 }
 }
?&gt;
</pre>
<h2>Some &#8216;Gotchas&#8217;</h2>
<p>The two main problems I had in using the LiveDocx service had nothing to do with LiveDocx or Zend_Service_LiveDocx (which has great documentation). The problems I had were in making MS Word do what I wanted in the template creation. Admittedly this may have more to do with the fact that I&#8217;d never created a mail merge template before but I thought I&#8217;d mention the two issues here. The issues were in adding merge fields to the document and creating bookmarks.</p>
<h3>Adding Merge Fields</h3>
<p>Merge fields are denoted by the text {MERGEFIELD fieldName} (where &#8216;fieldName&#8217; is the unique name of the field). What the documentation doesn&#8217;t say however is that the curly braces cannot be simply typed on the keyboard. In Word you need to enter ctrl + F9 to get a special set of merge field curly braces. In Word 2007 you can also go to the insert tab then Quick Parts, [field] and select merge field. Add a name for the merge field and it is inserted into the document where your cursor is placed. This second method makes the field look like «fieldName» but this is completely equivalent to the first method with the curly braces.</p>
<h3>Adding Bookmarks</h3>
<p>With LiveDocx a bookmarked section allows you to iterate over a section of a template, assigning content from an array to merge fields inside the bookmark on each iteration. This enables you to produce sections of documents that have the same structure but different content. This is obviously very handy as varying amounts of content can be assigned to the section of the template. One of the examples in the Zend Framework utilises this to produce a telephone bill where a varying number of calls can be assigned to a bookmarked section, producing a table of all the calls in the final document. The array has to be multi-dimensional, with the top level array being numeric and each nested array being associative where the key is the name of the merge field and the value is the content to be inserted. What the documentation doesn&#8217;t tell you is how to create the bookmarks (there&#8217;s supposed to be a screenshot in the documentation which is missing) and I spent a few frustrating hours before I finally found out how to do it. Here are the steps.</p>
<ol>
<li>Go to the insert tab in Word and select bookmark.</li>
<li>Create a new bookmark that begins with blockStart_ (ie. blockStart_bookmarkName).</li>
<li>The bookmark will be invisible in the document but go ahead and add your merge fields with whatever formatting you like.</li>
<li>After the last merge field for the section add another bookmark but start it with blockEnd_ (ie. blockEnd_bookmarkName).</li>
<li>When you assign content to the bookmarked section make sure that the array is assigned to the name of the bookmark, like the following:</li>
</ol>
<pre class="brush: php; title: ; notranslate">

&lt;?php

//Assume $doc is a Zend_Service_LiveDocx_MailMerge object and $array is the multidimensional array to be assigned to the bookmark.

$doc-&gt;assign('bookmarkName', $array);

?&gt;
</pre>
<h2>Conclusion</h2>
<p>I hope you find this brief introduction to LiveDocx useful. I&#8217;ve found it to be a great service that&#8217;s easy to use and I&#8217;ll certainly use it again the next time I need to produce professional quality documents from within PHP.</p>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2010/05/16/creating-professional-documents-the-easy-way/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Location Aware Webpages</title>
		<link>http://jeremycook.ca/2010/02/13/location-aware-webpages/</link>
		<comments>http://jeremycook.ca/2010/02/13/location-aware-webpages/#comments</comments>
		<pubDate>Sat, 13 Feb 2010 20:55:55 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Services]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=54</guid>
		<description><![CDATA[This post discusses a how I implemented a solution for a client which shows which of the client's stores a user visiting a website is geographically close to.]]></description>
			<content:encoded><![CDATA[<p>I&#8217;m working on a site for a client at the moment who has around 35 store locations across Canada. The client was keen to have a &#8216;your local store&#8217; feature on the home page where the store closest to the visitors location was featured. I thought I&#8217;d write briefly about this, the solution I came up with to address this problem, and some of the limitations of it.</p>
<p>To display the local store information I needed to go through a number of distinct steps in the code.</p>
<ol>
<li>Determine the visitors location from their IP address.</li>
<li>If the visitor is in Canada calculate the distance to their closest store.</li>
<li>If they&#8217;re within 100km of a store display information on that store on the homepage.</li>
<li>If the user is not in Canada, is more than 100km away from a store or if there is any kind of error display a generic message about store locations.</li>
</ol>
<h2>Getting the Users Location</h2>
<p>Determining a visitors location from their IP address can be done through a number of GeoIp services, some available for free and some paid for. Ideally I wanted to use a PECL <a href="http://www.php.net/manual/en/book.geoip.php">PHP extension</a> to do the location lookup but this was not possible as I could not persuade my web host to install this. I fell back to using a free web service provided by <a href="http://ipinfodb.com/">ipinfodb</a> for the lookup. I found a class on PHPClasses that used this web service, which I substantially rewrote to serve my purposes. The code that performs the lookup in the class is below:</p>
<pre class="brush: php; title: ; notranslate">

$strAPIURL = $this-&gt;apiUrl . &quot;?ip=&quot; . urlencode ($this-&gt;remoteAddress);
 //Make a call to the api and fetch the result.
 $ch        = curl_init ($strAPIURL);
 curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
 $xmlResult = curl_exec ($ch);
 curl_close ($ch);

 if ($xmlResult &amp;&amp; strlen ($xmlResult) &gt; 2) {
 //Process the result
 $result = simplexml_load_string ($xmlResult);

 if ((string)$result-&gt;Status == 'OK') {
 $this-&gt;countryName   = (string)$result-&gt;CountryName;
 $this-&gt;countryCode   = (string)$result-&gt;CountryCode;
 $this-&gt;cityName      = (string)$result-&gt;City;
 $this-&gt;zipPostalCode = (string)$result-&gt;ZipPostalCode;
 $this-&gt;regionName    = (string)$result-&gt;RegionName;
 $this-&gt;regionCode    = (string)$result-&gt;RegionCode;
 $this-&gt;timezone      = (int)$result-&gt;Timezone;
 $this-&gt;gmtOffset     = (int)$result-&gt;Gmtoffset;
 $this-&gt;dstOffset     = (int)$result-&gt;Dstoffset;
 $this-&gt;lat           = (float)$result-&gt;Latitude;
 $this-&gt;long          = (float)$result-&gt;Longitude;
 //Set the ipSniffed flag to true
 $this-&gt;ipSniffed     = true;
 }
 }
</pre>
<p>Once the call to the web service has been completed the information returned is stored in public properties and a flag is set to show that a successful lookup was performed. My class uses filter_var() to make sure a valid IP address that is not in a private range is passed to it, throwing an InvalidArgument Exception if not. Of course, if an exception is thrown I fall back to my default position of displaying a generic message about store locations.</p>
<h2>Finding the Closest Store</h2>
<p>Once I have the users location from their IP address and I&#8217;ve determined that they&#8217;re visiting from Canada I proceed to calculate the distance to their closest store. This is done with a database query. Information about all of the stores is stored in a database table along with the latitude and longitude of each store. Using the latitude and longitude returned from the web service I can then calculate the users distance to the nearest store. This is done in the following code:</p>
<pre class="brush: php; title: ; notranslate">

protected function getClosestStore ($lat, $long) {
 $sql = &lt;&lt;&lt; _SQL_

SELECT StoreId, CONCAT_WS(', ', Address, City, CONCAT(Province, ' ', PostCode)) AS Address, Lat, Lon, ROUND(6371 * 2 * ASIN(SQRT(POWER(SIN((:lat - abs(lat)) * pi()/180 / 2), 2) +
COS(:lat * pi()/180 ) * COS(abs(lat) * pi()/180) *  POWER(SIN((:lon - lon) * pi()/180 / 2), 2) )),2)
AS Distance
FROM Stores
HAVING Distance &lt;= 100
ORDER BY Distance Limit 1
_SQL_;
 try {
 $db = db::getConn();
 $st = $db-&gt;prepare($sql);
 $st-&gt;execute(array(':lat' =&gt; $lat, ':lon' =&gt; $long));
 $this-&gt;storeInfo = $st-&gt;fetch(PDO::FETCH_ASSOC);
 }
 catch (PDOException $e) {
 error_log($e-&gt;getMessage());
 }
 }
</pre>
<p>This PHP in this code simply connects to the database (db::getConn() is a static method that returns a singleton instance of a PDO object), performs the query and stores the result in the storeInfo property. If no result is returned from the database the fetch() method will return false, which then tells me that a user is not within 100 km of a store. The real meat of this code is in the SQL query. It selects some information about the store and then uses some math to calculate the distance to the nearest store. This is done using the Haversine formula, which calculates the distance between two sets of latitude and longitude, taking into account the curvature of the earth. I&#8217;m not going to try to explain the math (mostly because I don&#8217;t fully understand it myself!) but there is a <a href="http://en.wikipedia.org/wiki/Haversine_formula">Wikipedia article</a> on the formula for anyone who is interested. The query then limits the results to stores that are within 100km, orders the results by distance to make sure the closest one is listed first and then returns the first result. If a result is returned I then record the StoreId of the closest store in a cookie, which is then used when the visitor visits the site again. This is to cut down on processing time for subsequent request for the home page and means that I won&#8217;t need to hit the ipinfodb web service on every request for the index page of the site.</p>
<h2>Limitations of this Solution</h2>
<p>There are three major limitations to this solution that I can see: the accuracy of GeoIP services, the database query and saving the local store information in a cookie.</p>
<p>GeoIP services claim about an 80% accuracy when tracking the geographic location of an IP address down to the city level (accuracy is about 95% for finding the country a user is visiting from). For example I am writing this in Guelph, Ontario but the IP address I am connected to the internet with resolves geographically to the nearby city of Kitchener. This is not a major problem for my application as the client only has 35 stores across Canada and I am just trying to find the closest one to a user. Given the small number of stores chances are that I will hit on the closest one, even allowing for only 80% accuracy in GeoIP tracking. For other applications this could be a problem. The W3C has a draft <a href="http://dev.w3.org/geo/api/spec-source.html">Geolocation API</a> which some browsers (such as Firefox) are starting to implement. Using this (perhaps with some AJAX) could help to get a more accurate fix on some users locations but would open up some privacy concerns. For this application I display links that allow a user to manually select their closest store if the GeoIP tracking is wrong, hopefully alleviating any problems caused by the 80% accuracy.</p>
<p>The SQL query as I&#8217;ve written it would have performance issues if a large number of locations were being stored. To calculate the distance the query has to find the distance to every store held in the database, only then narrowing it down to the closest. This is not a problem currently as the client only has 35 locations, but if hundreds or thousands of locations were being stored the query would be extremely innefficient and take a long time to execute. In the course of my research I did read something about limiting the search to a radius (100 km in this case). This would involve several queries and probably a stored procedure to carry them out in. As I was dealing with a small number of locations I decided against this for simplicity. For another solution involving more locations I would probably go with this approach.</p>
<p>Saving the information on the users local store in a cookie makes the application more efficient but it potentially slightly degrades the user experience. In a scenario where a user travels and expects to see information on the closest store to where they currently are this would not work. I made a judgement call here and decided that it was better to go with the more efficient approach for this client, but in other cases this may not be so. Of course, if I could install the PECL GeoIP extension I wouldn&#8217;t need to store the cookie. Looking up a users location using this extension would be no more complicated than calling some PHP functions. The GeoIP information would be stored locally as part of the extension. This would be quicker than creating a call to and processing a response from a web service. Unfortunately this was not an option for me on this occasion. This problem can be overcome by the user manually selecting their closest store, overriding the mechanism I programmed.</p>
<p>All in all I am fairly happy with the solution I arrived at for this problem. It enables me to tailor content on the page based on where a user is. My example is a fairly simple one but it would be possible to do far more sophisticated things using GeoIP such as automatically displaying content in different languages depending on which country a visitor is coming from or displaying prices in different currencies. Due to the less than 100% accuracy of GeoIP mechanisms would always need to be provided to allow a user to override the conclusions found by the code though.</p>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2010/02/13/location-aware-webpages/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Tracking Social Media Marketing</title>
		<link>http://jeremycook.ca/2010/02/01/tracking-social-media-marketing/</link>
		<comments>http://jeremycook.ca/2010/02/01/tracking-social-media-marketing/#comments</comments>
		<pubDate>Tue, 02 Feb 2010 02:43:59 +0000</pubDate>
		<dc:creator>Jeremy Cook</dc:creator>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Social Media]]></category>
		<category><![CDATA[Web Development]]></category>
		<category><![CDATA[Web Services]]></category>

		<guid isPermaLink="false">http://jeremycook.ca/?p=29</guid>
		<description><![CDATA[How many businesses really know what is being said about them on social networks? This post looks at how easy it is to perform a search on Twitter using the Twitter search API and analyse all of the tweets returned using the Yahoo term extraction API.]]></description>
			<content:encoded><![CDATA[<p>Many businesses are now using social media services such as Twitter for marketing. It can be a great tool for reaching a wide user base but I wonder how many businesses really know what is being said about them on social networks. It seems to me that if someone wants to use a tool like Twitter for marketing they need to have a way to monitor what sort of response their marketing efforts are generating. Business owners wouldn&#8217;t dream of spending time, effort and money on any other form of marketing without knowing what sort of response is being generated.</p>
<p>Thankfully all of the main social networks provide web service API&#8217;s and using these it&#8217;s possible to track just about any metric that you would like. As an example I&#8217;ve put together a short (about 100 lines including some nice formatting and comments) script that carries out a search on Twitter, using the Twitter search API, and then extracts keywords from each tweet returned, using the Yahoo term extraction API. The results are sent to the user as a CSV file containing columns with the date the tweet was posted, the author, the tweet text and the keywords extracted for that tweet. Popular keywords are then presented at the bottom of the file. This is by no means production ready code but is really a quick and dirty test to show the kind of thing that is possible. Here&#8217;s a brief look at how it works.</p>
<pre class="brush: php; title: ; notranslate">

require 'termExtractor.php';

try {
 //Perform the initial search on Twitter. Also set the number of tweets to return as the maximum allowed per page (100)
 $search = '&quot;Demo Camp Guelph&quot; OR #dcg';
 $query  = http_build_query(array (
 'q'     =&gt; $search,
 'lang'  =&gt; 'en',
 'rpp'   =&gt; 100,
 'since' =&gt; date('Y-m-d', strtotime('5 days ago'))
 ));
 $url    = 'http://search.twitter.com/search.json?';
 $ch     = curl_init($url . $query);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 $tweets = json_decode(curl_exec($ch));
</pre>
<p>The beginning of the script includes a class that I wrote to handle working with the <a href="http://developer.yahoo.com/search/content/V1/termExtraction.html">Yahoo term extraction API</a>. It then sets up a few variables and carries out an initial search using the Twitter API. I&#8217;ve chosen as my search query text relating to a meeting of developers that took place in Guelph last week. Documentation on the <a href="http://apiwiki.twitter.com/Twitter-Search-API-Method%3A-search">Twitter search API</a> is pretty good and includes information on the format of the search terms, including modifiers that are allowed. My search looks for any tweets posted in the last five days with the text &#8216;Demo Camp Guelph&#8217; or the hashtag &#8216;#dcg&#8217;. It also sets the number of results to return to 100. This is the maximum number that can be returned from a request but Twitter provides a mechanism whereby searches with more than 100 results can be paged over.</p>
<pre class="brush: php; title: ; notranslate">

//Open the PHP output as a file handle
 $output = fopen('php://output', 'w');
 if (!$output)
 throw new Exception('Unable to bind PHP output as a file handle');
 //Set headers to tell the browser a csv attachement is coming. Also set cache control to no-cache to ensure browser does not cache the results file.
 header ('Content-Type: application/csv');
 header ('Content-Disposition: attachment; filename=&quot;Twitter_Search.csv&quot;');
 header ('Cache-Control: no-cache, must-revalidate');
 //Add headings for the columns
 fputcsv($output, array (
 'Posted On',
 'Author',
 'Tweet',
 'Keywords'
 ));
 if (!isset($tweets-&gt;results) || !count($tweets-&gt;results)) {
 //No search results. Output an error message and exit
 fputcsv($output, array('Sorry, there are no results that match that search'));
 curl_close ($ch);
 fclose ($output);
 exit;
 }
</pre>
<p>This code binds the output of the script to a file handle, allowing me to present the output of the script as a file to the browser. HTTP headers are also set to tell the browser to expect a csv file and that it should be treated as an attachment. Some headings for columns are then written and if there are no search results the script outputs a message and exits.</p>
<pre class="brush: php; title: ; notranslate">

//Set up the termExtractor object, an array to hold the keywords results and a flag to show that we're in the first iteration of the do while loop
 $keys = new termExtractor(YAHOO_API_KEY);
 $keywords_array = array();
 $first = true;
 //Loop over the results
 do {
 if (!$first) {
 //Break the query string for the next page into its' parts. Use substr() to remove the initial ?
 parse_str (substr($tweets-&gt;next_page, 1));
 $query = http_build_query(array (
 'q'      =&gt; $q,
 'page'   =&gt; $page,
 'max_id' =&gt; $max_id,
 'lang'   =&gt; 'en',
 'rpp'    =&gt; 100
 ));
 curl_setopt($ch, CURLOPT_URL, $url . $query);
 $tweets = json_decode(curl_exec($ch));
 }
 foreach ($tweets-&gt;results as $result) {
 //Result text from Twitter is utf-8 encoded and htmlentity encoded.
 $result-&gt;text = html_entity_decode(utf8_decode($result-&gt;text));
 $keywords     = $keys-&gt;doTextQuery($result-&gt;text);
 //If a single keyword was returned turn it into an array to save repetition of code.
 if (is_string($keywords) &amp;&amp; strlen($keywords)) {
 $keywords = array($keywords);
 }
 if (is_array($keywords)) {
 foreach ($keywords as $word) {
 //Check that the keyword does not appear in the initial search term.
 switch (!stristr($search, $word)) {
 case true:
 //If the keyword has already been set in the array increment the count by 1
 if (array_key_exists($word, $keywords_array)) {
 $keywords_array[$word]++;
 } else {
 $keywords_array[$word] = 1;
 }
 break;
 default:
 continue;
 }
 }
 $keywords = implode(', ', $keywords);
 }
 fputcsv($output, array (
 $result-&gt;created_at,
 $result-&gt;from_user,
 $result-&gt;text,
 $keywords
 ));
 }
 $first = false;
 } while (isset($tweets-&gt;next_page));
</pre>
<p>This code sets up the term extraction object and then loops over the results while there is a next_page available. Keywords are output to the CSV file alongside each tweet and also stored in an array for later use. Keywords which appear in the original search query are filtered out of the results array and using a do while loop ensures that the first set of search results are output even if there are no other results after that.</p>
<pre class="brush: php; title: ; notranslate">

//Destroying the $keys object triggers the destructor which disposes of the cURL resource in the object
$keys = NULL;
//Free up the cURL resource
 curl_close ($ch);
 fputcsv($output, array('Keywords for this search:'));
 fputcsv($output, array (
 'Phrase',
 'Number of Occurences'
 ));
 foreach ($keywords_array as $word =&gt; $count) {
 if ($count &gt;= 3) {
 fputcsv($output, array (
 $word,
 $count
 ));
 }
 }
 fclose ($output);
} catch (Exception $e) {
 echo $e-&gt;getTraceAsString();
}
</pre>
<p>The final part of the code processes the stored keywords and outputs any keyword that appears three times or more in the tweets that have been found.</p>
<p>The code here is obviously not optimal but is merely meant as an example of what is possible. It would be very easy to abstract the logic out of this script into a Twitter search class, making the code portable and reusable. The code also takes quite a long time to execute, depending on the search term used, the number of tweets returned and the time frame to return results for. The problem seems to be in the code that calls the Yahoo web service. Every &#8216;page&#8217; of the Twitter search results returns up to 100 tweets and the text of each of these must then be passed onto Yahoo for analysis. This means that for each call to the Twitter search API there are up to 100 calls to the Yahoo term extraction service. This can easily cause the script to &#8216;time out&#8217;. In its&#8217; present form it would probably be best run once a day to return search results for the last 24 hours, with the results being emailed to someone to analyse. It may be possible to amalgamate the calls to the Yahoo service into one. The keyword extraction itself is somewhat rudimentary and could probably be easily improved, depending on what a client was looking to do.</p>
<p>What is interesting to me is how powerful this could be. It could be possible to build automated scripts that run on a schedule, tracking campaigns on Twitter. If the text analysis could be made more sophisticated it would be possible to alert someone only if certain trends appeared, allowing them to then make appropriate responses. This would then become a very useful Twitter tracking tool. Similar tools could be built for other social networking sites.</p>
<p>As an example of what is possible this script serves its&#8217; purpose. For a production environment there are lots of ways in which it could be improved but the possibilities are there. Source code and an example of the script output can be downloaded <a href="http://jeremycook.ca/wp-content/uploads/2010/02/Twitter_search_code.zip">here</a> but due to the issues with time outs I can&#8217;t make the script itself available to request from a browser. I hope someone will be inspired to come up with their own, better, Twitter analysis tools and will consider sharing it with the rest of us.</p>
]]></content:encoded>
			<wfw:commentRss>http://jeremycook.ca/2010/02/01/tracking-social-media-marketing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>

