<?php

/**
 * twitterFeed Class
 * 
 * This class can be used to request data in XML format from Twitter
 * without access to the API.  It requires:
 *     PEAR Cache:    http://pear.php.net/package/Cache
 *     PHP cURL:      http://www.php.net/curl
 *     PHP SimpleXML: http://www.php.net/simplexml
 * 
 * The cache can be reset by adding a URL parameter "reset_cache" to
 * the page on which the feed is used.
 * 
 * Basic usage (as it works on patrick-oneill.com) ...
 * 
 *         $twitterFeed = new twitterFeed;
 *         $twitterFeed->getXml('pgoneill');
 *         $twitterData = $twitterFeed->xmlToArray(5);
 * 
 * This is, of course, assuming default options are used.  Results in
 * a multidimensional array of data culled from your Twitter XML feed.
 * 
 * @package 
 * @author Patrick O'Neill
 * @copyright Patrick O'Neill 2008
 * @version 1.0
 * @access public
 */

class twitterFeed {

    
private $cache_time$cache_dir$xml$rel;

    
/**
     * twitterFeed::__construct()
     * 
     * The constructor sets base values for configurable data members.
     * 
     */
    
public function __construct() {
        
$this->cache_time 3600;
        
$this->cache_dir  $_SERVER["DOCUMENT_ROOT"] . "/cache";
        
$this->rel        'nofollow';
    } 
//END __construct


    /**
     * twitterFeed::setCacheTime()
     * 
     * This method can be used to alter the duration of the XML cache.
     * 
     * @param mixed $path
     * @return boolean True on success or throws an exception.
     */
    
public function setCacheTime($minutes) {
        
try {
            if (!
is_int($minutes) || $minutes 1) {
                
throw new Exception($this->formatException('Please provide an integer value, greater than 1, in minutes for cache time.'));
            }
            
$this->cache_time $minutes 60;
            return 
true;
        }
        
catch (Exception $exception) {
            echo 
$exception->getMessage();
        }
    } 
//END setCacheTime


    /**
     * twitterFeed::setCacheDir()
     * 
     * This method can be used to set the directory where the XML
     * data is stored
     * 
     * @param string $path
     * @return boolean True on success or throws an exception.
     */
    
public function setCacheDir($path) {
        
try {
            if (!
is_dir($path) || !is_string($path)) {
                
throw new Exception($this->formatException('Cache directory does not exist.'));
            }
            
$this->cache_dir $path;
            return 
true;
        }
        
catch (Exception $exception) {
            echo 
$exception->getMessage();
        }
    } 
//END setCacheDir


    /**
     * twitterFeed::setRel()
     * 
     * This method sets the value of the rel attributes on links 
     * that are generated within tweets.
     * 
     * @param mixed $rel
     * @return boolean True.
     */
    
public function setRel($rel) {
        
$this->rel $rel;
        return 
true;
    } 
//END setRel


    /**
     * twitterFeed::getXml()
     * 
     * This method retrieves XML data either from the cache or
     * from Twitter itself.  It returns the XML, but you really 
     * don't need to do anything with it.  It's there in case you
     * want to alter it in some way.
     * 
     * @param mixed $user
     * @return string The XML data.
     */
    
public function getXml($user) {
        
try {
            
// Make sure PEAR Cache is working.
            
if (!require_once("Cache.php")) {
                
throw new Exception($this->formatException('You must have the PEAR Cache module installed!'));
            }
            
// Make sure we can cURL.
            
elseif (!function_exists('curl_init')) {
                
throw new Exception($this->formatException('You must have the PHP cURL module installed!'));
            }
            
// Make sure the cache directory exists.
            
elseif (!is_dir($this->cache_dir)) {
                
throw new Exception($this->formatException('Cache directory does not exist.'));
            }
            
// Good to go...
            
else {
                
// Set up options and instantiate the cache class.
                
$options    = array("cache_dir" => $this->cache_dir);
                
$cache      = new cache("file"$options);
                
$cached_id  $cache->generateID('twitterFeedCacheFor'.$user);

                
// Try to find a cached version or check if we want to reset the cache.
                
if (!($this->xml $cache->get($cached_id)) || isset($_GET['reset_cache'])) {
                    
// Make a curl request to grab the user's Twitter data.
                    
$curl curl_init('http://twitter.com/statuses/user_timeline/' $user '.xml');
                    
curl_setopt($curl,CURLOPT_HEADER,false);
                    
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
                    
$this->xml curl_exec($curl);
                    
curl_close($curl);
                    
// Save the data in a cache file.
                    
$cache->save($cached_id$this->xml$this->cache_time);
                }
                return 
$this->xml;
            }
        }
        
catch (Exception $exception) {
            echo 
$exception->getMessage();
        }
    } 
//END getXml


    /**
     * twitterFeed::xmlToArray()
     * 
     * This method takes the XML data gathered by twitterFeed::getXML()
     * and turns it into a multidimensional array. 
     * 
     * @param integer $limit
     * @return array The array culled from the XML.
     */
    
public function xmlToArray($limit 20) {
        
try {
            if (!
function_exists('simplexml_load_string')) {
                
throw new Exception($this->formatException('You must have the PHP SimpleXML module installed!'));
            }
            elseif (!
is_int($limit) || $limit 20) {
                
throw new Exception($this->formatException('If you want to alter the number of tweets, you must specify an integer less than 20.'));
            }
            else {
                
$data simplexml_load_string($this->xml);

                
// Set up an array to store tweets and go ahead and store
                // the Twitter user's data now.
                
$twitter = array(
                    
'tweets' => array(),
                    
'user' => array(
                        
'id' => intval($data->status[0]->user->id),
                        
'name' => strval($data->status[0]->user->name),
                        
'screen_name' => strval($data->status[0]->user->screen_name),
                        
'location' => strval($data->status[0]->user->location),
                        
'description' => strval($data->status[0]->user->description),
                        
'image_url_small' => strval($data->status[0]->user->profile_image_url),
                        
'image_url_large' => str_replace("normal.jpg","bigger.jpg",$data->status[0]->user->profile_image_url),
                        
'url' => strval($data->status[0]->user->url),
                        
'followers' => intval($data->status[0]->user->followers_count)
                    )
                );

                
// Run a loop to the number of tweets the user requested.
                
$i 0;
                while (
$i $limit) {
                    
$twitter['tweets'][$i]['id'] = intval($data->status[$i]->id);
                    
$twitter['tweets'][$i]['link'] = 'http://twitter.com/'.$twitter['user']['screen_name'].'/statuses/'.$twitter['tweets'][$i]['id'];
                    
$twitter['tweets'][$i]['unix_timestamp'] = strtotime($data->status[$i]->created_at);
                    
$twitter['tweets'][$i]['time_ago'] = $this->timeDiff($twitter['tweets'][$i]['unix_timestamp']);
                    
$twitter['tweets'][$i]['source'] = strval($data->status[$i]->source);
                    
// Replace "@something" with a link to that profile after 
                    // parsing out URLs from the text.
                    
$twitter['tweets'][$i]['text'] = preg_replace('|@([a-z0-9_]+)|i''@<a href="http://twitter.com/\\1">\\1</a>'$this->replaceUrls($data->status[$i]->text));
                    
$i++;
                }
                return 
$twitter;
            }
        }
        
catch (Exception $exception) {
            echo 
$exception->getMessage();
        }
    } 
//END xmlToArray


    /**
     * twitterFeed::formatException()
     * 
     * This purely internal method wraps exceptions in HTML.
     * 
     * @param string $message
     * @return string Formatted message.
     */
    
private function formatException($message) {
        return 
'<p class="exception"><strong>class.twitterFeed Error:</strong> '.$message.'</p>';
    }


    
/**
     * twitterFeed::replaceUrls()
     * 
     * This internal method parses hyperlinks out of text.
     * 
     * @param mixed $string
     * @return string
     */
    
private function replaceUrls($string) {
        
$host "([a-z\d][-a-z\d]*[a-z\d]\.)+[a-z][-a-z\d]*[a-z]";
        
$port "(:\d{1,})?";
        
$path "(\/[^?<>\#\"\s]+)?";
        
$query "(\?[^<>\#\"\s]+)?";
        return 
preg_replace("#((ht|f)tps?:\/\/{$host}{$port}{$path}{$query})#i""<a href=\"$1\" rel=\"".$this->rel."\">$1</a>"$string);
    } 
//END replaceUrls


    /**
     * twitterFeed::timeDiff()
     * 
     * This internal method parses a Unix timestamp into
     * a differential format. E.g. "5 days ago"
     * 
     * @param mixed $time
     * @param mixed $opt
     * @return string
     */
    
private function timeDiff($time$opt = array()) {
        
// The default values
        
$defOptions = array(
            
'to' => 0,
            
'parts' => 1,
            
'precision' => 'second',
            
'distance' => TRUE,
            
'separator' => ', '
        
);
        
$opt array_merge($defOptions$opt);
        
// Default to current time if no to point is given
        
(!$opt['to']) && ($opt['to'] = time());
        
// Init an empty string
        
$str '';
        
// To or From computation
        
$diff = ($opt['to'] > $time) ? $opt['to']-$time $time-$opt['to'];
        
// An array of label => periods of seconds;
        
$periods = array(
            
'decade' => 315569260,
            
'year' => 31556926,
            
'month' => 2629744,
            
'week' => 604800,
            
'day' => 86400,
            
'hour' => 3600,
            
'minute' => 60,
            
'second' => 1
        
);
        
// Round to precision
        
if ($opt['precision'] != 'second')
            
$diff round(($diff/$periods[$opt['precision']])) * $periods[$opt['precision']];
        
// Report the value is 'less than 1 ' precision period away
        
(== $diff) && ($str 'less than 1 '.$opt['precision']);
        
// Loop over each period
        
foreach ($periods as $label => $value) {
            
// Stitch together the time difference string
            
(($x=floor($diff/$value))&&$opt['parts']--) && $str.=($str?$opt['separator']:'').($x.' '.$label.($x>1?'s':''));
            
// Stop processing if no more parts are going to be reported.
            
if ($opt['parts'] == || $label == $opt['precision']) break;
            
// Get ready for the next pass
            
$diff -= $x*$value;
        }
        
$opt['distance'] && $str.=($str&&$opt['to']>$time)?' ago':' away';
        return 
$str;
    }
}