Parsing Twitter Feeds with PHP

The most difficult part of pulling information from Twitter's 1.1 API is the actual request. I've covered how to create the OAuth request in some previous posts: making a basic OAuth request and passing in extra parameters. Once you get the information back, though, what do you do with it?

Twitter will return JSON for their requests, which is easy to parse with PHP and allows for some relatively deep nesting of data. You'll need to decode it first with a simple function call.

  1. $response = curl_exec($curl_request); // from the previous posts

  2. $response = json_decode($response);

Below is a dump of a response after being decoded into an associative array. I limited the response, as each tweet object can take up a lot of room.

  1. array(20) {

  2. [0]=>

  3. object(stdClass)#25 (21) {

  4. ["created_at"]=>

  5. string(30) "Sun Feb 24 15:15:50 +0000 2013"

  6. ["id"]=>

  7. float(3.0569755089004E+17)

  8. ["id_str"]=>

  9. string(18) "305697550890041344"

  10. ["text"]=>

  11. string(87) "Think I'm failing at the weekend thing. (@ Blue Door Consulting) http://t.co/JY3XSRGnyT"

  12. ["source"]=>

  13. string(61) "foursquare"

  14. ["truncated"]=>

  15. bool(false)

  16. ["in_reply_to_status_id"]=>

  17. NULL

  18. ["in_reply_to_status_id_str"]=>

  19. NULL

  20. ["in_reply_to_user_id"]=>

  21. NULL

  22. ["in_reply_to_user_id_str"]=>

  23. NULL

  24. ["in_reply_to_screen_name"]=>

  25. NULL

  26. ["user"]=>

  27. object(stdClass)#24 (39) {

  28. ["id"]=>

  29. int(26515074)

  30. ["id_str"]=>

  31. string(8) "26515074"

  32. ["name"]=>

  33. string(13) "Jacob Emerick"

  34. ["screen_name"]=>

  35. string(8) "jpemeric"

  36. ["location"]=>

  37. string(12) "Appleton, WI"

  38. ["description"]=>

  39. string(132) "I'm a web programmer, developer, hiker, innovator, and a young man. Currently working at Blue Door Consulting as a programming poet."

  40. ["url"]=>

  41. string(29) "https://home.jacobemerick.com/"

  42. ["entities"]=>

  43. object(stdClass)#23 (2) {

  44. ["url"]=>

  45. object(stdClass)#22 (1) {

  46. ["urls"]=>

  47. array(1) {

  48. [0]=>

  49. object(stdClass)#21 (3) {

  50. ["url"]=>

  51. string(29) "https://home.jacobemerick.com/"

  52. ["expanded_url"]=>

  53. NULL

  54. ["indices"]=>

  55. array(2) {

  56. [0]=>

  57. int(0)

  58. [1]=>

  59. int(29)

  60. }

  61. }

  62. }

  63. }

  64. ["description"]=>

  65. object(stdClass)#20 (1) {

  66. ["urls"]=>

  67. array(0) {

  68. }

  69. }

  70. }

  71. ["protected"]=>

  72. bool(false)

  73. ["followers_count"]=>

  74. int(269)

  75. ["friends_count"]=>

  76. int(259)

  77. ["listed_count"]=>

  78. int(19)

  79. ["created_at"]=>

  80. string(30) "Wed Mar 25 15:06:41 +0000 2009"

  81. ["favourites_count"]=>

  82. int(20)

  83. ["utc_offset"]=>

  84. int(-21600)

  85. ["time_zone"]=>

  86. string(26) "Central Time (US & Canada)"

  87. ["geo_enabled"]=>

  88. bool(false)

  89. ["verified"]=>

  90. bool(false)

  91. ["statuses_count"]=>

  92. int(4368)

  93. ["lang"]=>

  94. string(2) "en"

  95. ["contributors_enabled"]=>

  96. bool(false)

  97. ["is_translator"]=>

  98. bool(false)

  99. ["profile_background_color"]=>

  100. string(6) "FAFAFA"

  101. ["profile_background_image_url"]=>

  102. string(81) "http://a0.twimg.com/profile_background_images/333242976/huron-mountain-sunset.jpg"

  103. ["profile_background_image_url_https"]=>

  104. string(83) "https://si0.twimg.com/profile_background_images/333242976/huron-mountain-sunset.jpg"

  105. ["profile_background_tile"]=>

  106. bool(false)

  107. ["profile_image_url"]=>

  108. string(78) "http://a0.twimg.com/profile_images/2286764626/xnzz7ejexbfr3p8jwarp_normal.jpeg"

  109. ["profile_image_url_https"]=>

  110. string(80) "https://si0.twimg.com/profile_images/2286764626/xnzz7ejexbfr3p8jwarp_normal.jpeg"

  111. ["profile_banner_url"]=>

  112. string(57) "https://si0.twimg.com/profile_banners/26515074/1347979073"

  113. ["profile_link_color"]=>

  114. string(6) "098F99"

  115. ["profile_sidebar_border_color"]=>

  116. string(6) "B5B5B5"

  117. ["profile_sidebar_fill_color"]=>

  118. string(6) "D8D8D8"

  119. ["profile_text_color"]=>

  120. string(6) "000000"

  121. ["profile_use_background_image"]=>

  122. bool(true)

  123. ["default_profile"]=>

  124. bool(false)

  125. ["default_profile_image"]=>

  126. bool(false)

  127. ["following"]=>

  128. bool(false)

  129. ["follow_request_sent"]=>

  130. bool(false)

  131. ["notifications"]=>

  132. bool(false)

  133. }

  134. ["geo"]=>

  135. NULL

  136. ["coordinates"]=>

  137. NULL

  138. ["place"]=>

  139. NULL

  140. ["contributors"]=>

  141. NULL

  142. ["retweet_count"]=>

  143. int(0)

  144. ["entities"]=>

  145. object(stdClass)#19 (3) {

  146. ["hashtags"]=>

  147. array(0) {

  148. }

  149. ["urls"]=>

  150. array(1) {

  151. [0]=>

  152. object(stdClass)#18 (4) {

  153. ["url"]=>

  154. string(22) "http://t.co/JY3XSRGnyT"

  155. ["expanded_url"]=>

  156. string(21) "http://4sq.com/XUtlYf"

  157. ["display_url"]=>

  158. string(14) "4sq.com/XUtlYf"

  159. ["indices"]=>

  160. array(2) {

  161. [0]=>

  162. int(65)

  163. [1]=>

  164. int(87)

  165. }

  166. }

  167. }

  168. ["user_mentions"]=>

  169. array(0) {

  170. }

  171. }

  172. ["favorited"]=>

  173. bool(false)

  174. ["retweeted"]=>

  175. bool(false)

  176. ["possibly_sensitive"]=>

  177. bool(false)

  178. }

  179. [1]=>

  180. object(stdClass)#8 (21) {

  181. ["created_at"]=>

  182. string(30) "Sun Feb 24 01:01:37 +0000 2013"

  183. ["id"]=>

With the return being a standard PHP array it's really easy to loop through the individual entries. Each tweet can be referenced by a standard object call. For example, if you wanted to display the raw content of each tweet with the date it was posted you would do something like this.

  1. foreach($response as $tweet)

  2. {

  3. echo "{$tweet->text} {$tweet->created_at}\n";

  4. }

Enhancing the Tweet

If you just spit out the text property you are missing out on a lot, though. Tweets have a lot of rich entities attached to them, like usernames, urls, hashtags, and media objects. On twitter.com and well-formed twitter clients these entities will be attached by hyperlinks to other resources. You can click on a hashtag within a link to boot up a search for that term and check out a conversation. That's where the entities object comes in.

For each type of entity (user_mentions, hashtags, urls, and media) there is a collection of data to do basic 'search and replace' on the default text field. For example, each 'urls' object contains a 'url' (the before), 'expanded_url' (what Twitter recognizes as the pre-shortened URL), 'display_url' (what Twitter uses as the anchor text for each link), and a pair of indices for the start and end points of the recognized entity. You have everything you need to create a rich, linked tweet and not a flat piece of text.

There are two ways you can enhance each entity. The first obvious method would be a simple str_replace.

  1. foreach($response as $tweet)

  2. {

  3. foreach($tweet->entities->urls as $urls_object)

  4. {

  5. $search = "{$urls_object->url}";

  6. $replace = "<a href=\"{$urls_object->url}\" title=\"{$urls_object->expanded_url}\">{$urls_object->display_url}</a>";

  7. $text = str_replace($search, $replace, $text);

  8. }

  9. }

This works great until you run into more complex cases. If a tweet goes something like "This is a ridiculous but possible #case http://domain.com/use#case", then you're going to have a bad time. The previous chunk of code will replace both instances of '#case' and the may get all messed up (depending on what order you replace the entities). This is why Twitter includes the indices to let you replace defined pieces of the text and not depend on searching. You have to do the replacements in reverse order, though, or else you'll be replacing the wrong chunks of text.

  1. $hashtag_link_pattern = '<a href="http://twitter.com/search?q=%%23%s&src=hash" rel="nofollow" target="_blank">#%s</a>';

  2. $url_link_pattern = '<a href="%s" rel="nofollow" target="_blank" title="%s">%s</a>';

  3. $user_mention_link_pattern = '<a href="http://twitter.com/%s" rel="nofollow" target="_blank" title="%s">@%s</a>';

  4. $media_link_pattern = '<a href="%s" rel="nofollow" target="_blank" title="%s">%s</a>';

  5. foreach($response as $tweet)

  6. {

  7. $text = $tweet->text;

  8. $entity_holder = array();

  9. foreach($tweet->entities->hashtags as $hashtag)

  10. {

  11. $entity = new stdclass();

  12. $entity->start = $hashtag->indices[0];

  13. $entity->end = $hashtag->indices[1];

  14. $entity->length = $hashtag->indices[1] - $hashtag->indices[0];

  15. $entity->replace = sprintf($hashtag_link_pattern, strtolower($hashtag->text), $hashtag->text);

  16. $entity_holder[$entity->start] = $entity;

  17. }

  18. foreach($tweet->entities->urls as $url)

  19. {

  20. $entity = new stdclass();

  21. $entity->start = $url->indices[0];

  22. $entity->end = $url->indices[1];

  23. $entity->length = $url->indices[1] - $url->indices[0];

  24. $entity->replace = sprintf($url_link_pattern, $url->url, $url->expanded_url, $url->display_url);

  25. $entity_holder[$entity->start] = $entity;

  26. }

  27. foreach($tweet->entities->user_mentions as $user_mention)

  28. {

  29. $entity = new stdclass();

  30. $entity->start = $user_mention->indices[0];

  31. $entity->end = $user_mention->indices[1];

  32. $entity->length = $user_mention->indices[1] - $user_mention->indices[0];

  33. $entity->replace = sprintf($user_mention_link_pattern, strtolower($user_mention->screen_name), $user_mention->name, $user_mention->screen_name);

  34. $entity_holder[$entity->start] = $entity;

  35. }

  36. foreach($tweet->entities->media as $media)

  37. {

  38. $entity = new stdclass();

  39. $entity->start = $media->indices[0];

  40. $entity->end = $media->indices[1];

  41. $entity->length = $media->indices[1] - $media->indices[0];

  42. $entity->replace = sprintf($media_link_pattern, $media->url, $media->expanded_url, $media->display_url);

  43. $entity_holder[$entity->start] = $entity;

  44. }

  45. krsort($entity_holder);

  46. foreach($entity_holder as $entity)

  47. {

  48. $text = substr_replace($text, $entity->replace, $entity->start, $entity->length);

  49. }

  50. }

This is a bit verbose but allows a lot of flexibility in terms of formatting and updates. A few functions could make this quite a bit more DRY.
NOTE: if you have any issues with multibyte characters check out this comment for help.

More with Tweet Data

There is a lot more that you can do with the data returned by Twtter. User information, replies (conversations), retweet_count (limited metric of engagement), or even using the source field to display how you're sending in the tweets. One disappointing thing about the data is favorites - right now a tweet list does not contain a favorite count. You can get favorites from user objects or individual tweets, but not a list.

Anyways, I hope this helps out if you're planning on moving forward with Twitter integration. I've done many of these things on my lifestream website. Twitter is definitely one of the richer and easier APIs to deal with for reading once you get past the OAuth, especially compared to some of the nastier Google Data setups. Happy coding!