diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cdb3d20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +vendor/ +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..abac424 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php +php: + - "5.3" + +before_install: + - composer self-update + +before_script: + - composer install + +script: + - phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/README.md b/README.md index 6db3c5c..3fead7f 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,11 @@ twitter-api-php Simple PHP Wrapper for Twitter API v1.1 calls [![Total Downloads](https://poser.pugx.org/j7mbo/twitter-api-php/downloads.png)](https://packagist.org/packages/j7mbo/twitter-api-php) - +[![Build Status](https://travis-ci.org/J7mbo/twitter-api-php.svg?branch=master)](https://packagist.org/packages/j7mbo/twitter-api-php) **[Changelog](https://github.com/J7mbo/twitter-api-php/wiki/Changelog)** || **[Examples](https://github.com/J7mbo/twitter-api-php/wiki/Twitter-API-PHP-Wiki)** || -**[Wiki](https://github.com/J7mbo/twitter-api-php/wiki)** || -**[Buy me a beer!](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=KHQYGY4MM3E7J)** +**[Wiki](https://github.com/J7mbo/twitter-api-php/wiki)** [Instructions in StackOverflow post here](http://stackoverflow.com/questions/12916539/simplest-php-example-retrieving-user-timeline-with-twitter-api-version-1-1/15314662#15314662) with examples. This post shows you how to get your tokens and more. If you found it useful, please upvote / leave a comment! :) @@ -23,14 +22,14 @@ The aim of this class is simple. You need to: - Choose either GET / POST (depending on the request) - Choose the fields you want to send with the request (example: `array('screen_name' => 'usernameToBlock')`) -You really can't get much simpler than that. Here is an example of how to use the class for a POST request to block a user, and at the bottom is an example of a GET request. +You really can't get much simpler than that. The above bullet points are an example of how to use the class for a POST request to block a user, and at the bottom is an example of a GET request. Installation ------------ **Normally:** If you *don't* use composer, don't worry - just include TwitterAPIExchange.php in your application. -**Via Composer:** If you *do* use composer, here's what you add to your composer.json file to have TwitterAPIExchange.php automatically imported into your vendor's folder: +**Via Composer:** If you realise it's 2015 now and you *do* use composer, here's what you add to your composer.json file to have TwitterAPIExchange.php automatically imported into your vendor's folder: { "require": { diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index 7832725..71f8f59 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -13,21 +13,59 @@ */ class TwitterAPIExchange { + /** + * @var string + */ private $oauth_access_token; + + /** + * @var string + */ private $oauth_access_token_secret; + + /** + * @var string + */ private $consumer_key; + + /** + * @var string + */ private $consumer_secret; + + /** + * @var array + */ private $postfields; + + /** + * @var string + */ private $getfield; + + /** + * @var mixed + */ protected $oauth; + + /** + * @var string + */ public $url; + /** + * @var string + */ + public $requestMethod; + /** * Create the API access object. Requires an array of settings:: * oauth access token, oauth access token secret, consumer key, consumer secret * These are all available by creating your own application on dev.twitter.com * Requires the cURL library - * + * + * @throws \Exception When cURL isn't installed or incorrect settings parameters are provided + * * @param array $settings */ public function __construct(array $settings) @@ -50,12 +88,14 @@ public function __construct(array $settings) $this->consumer_key = $settings['consumer_key']; $this->consumer_secret = $settings['consumer_secret']; } - + /** * Set postfields array, example: array('screen_name' => 'J7mbo') - * + * * @param array $array Array of parameters to send to API - * + * + * @throws \Exception When you are trying to set both get and post fields + * * @return TwitterAPIExchange Instance of self for method chaining */ public function setPostfields(array $array) @@ -72,6 +112,11 @@ public function setPostfields(array $array) $this->postfields = $array; + // rebuild oAuth + if (isset($this->oauth['oauth_signature'])) { + $this->buildOauth($this->url, $this->requestMethod); + } + return $this; } @@ -79,6 +124,8 @@ public function setPostfields(array $array) * Set getfield string, example: '?screen_name=J7mbo' * * @param string $string Get key and value pairs as string + * + * @throws \Exception * * @return \TwitterAPIExchange Instance of self for method chaining */ @@ -121,9 +168,12 @@ public function getPostfields() /** * Build the Oauth object using params set in construct and additionals * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1 - * + * * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json * @param string $requestMethod Either POST or GET + * + * @throws \Exception + * * @return \TwitterAPIExchange Instance of self for method chaining */ public function buildOauth($url, $requestMethod) @@ -133,12 +183,12 @@ public function buildOauth($url, $requestMethod) throw new Exception('Request method must be either POST or GET'); } - $consumer_key = $this->consumer_key; - $consumer_secret = $this->consumer_secret; - $oauth_access_token = $this->oauth_access_token; + $consumer_key = $this->consumer_key; + $consumer_secret = $this->consumer_secret; + $oauth_access_token = $this->oauth_access_token; $oauth_access_token_secret = $this->oauth_access_token_secret; - $oauth = array( + $oauth = array( 'oauth_consumer_key' => $consumer_key, 'oauth_nonce' => time(), 'oauth_signature_method' => 'HMAC-SHA1', @@ -152,19 +202,34 @@ public function buildOauth($url, $requestMethod) if (!is_null($getfield)) { $getfields = str_replace('?', '', explode('&', $getfield)); + foreach ($getfields as $g) { $split = explode('=', $g); - $oauth[$split[0]] = $split[1]; + + /** In case a null is passed through **/ + if (isset($split[1])) + { + $oauth[$split[0]] = $split[1]; + } } } + $postfields = $this->getPostfields(); + + if (!is_null($postfields)) { + foreach ($postfields as $key => $value) { + $oauth[$key] = $value; + } + } + $base_info = $this->buildBaseString($url, $requestMethod, $oauth); $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret); $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); $oauth['oauth_signature'] = $oauth_signature; $this->url = $url; + $this->requestMethod = $requestMethod; $this->oauth = $oauth; return $this; @@ -173,7 +238,9 @@ public function buildOauth($url, $requestMethod) /** * Perform the actual data retrieval from the API * - * @param boolean $return If true, returns data. + * @param boolean $return If true, returns data. This is left in for backward compatibility reasons + * + * @throws \Exception * * @return string json If $return param is true, returns json data. */ @@ -183,13 +250,13 @@ public function performRequest($return = true) { throw new Exception('performRequest parameter must be true or false'); } - - $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); + + $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); $getfield = $this->getGetfield(); $postfields = $this->getPostfields(); - $options = array( + $options = array( CURLOPT_HTTPHEADER => $header, CURLOPT_HEADER => false, CURLOPT_URL => $this->url, @@ -199,7 +266,7 @@ public function performRequest($return = true) if (!is_null($postfields)) { - $options[CURLOPT_POSTFIELDS] = $postfields; + $options[CURLOPT_POSTFIELDS] = http_build_query($postfields); } else { @@ -214,7 +281,7 @@ public function performRequest($return = true) $json = curl_exec($feed); curl_close($feed); - if ($return) { return $json; } + return $json; } /** @@ -222,7 +289,7 @@ public function performRequest($return = true) * * @param string $baseURI * @param string $method - * @param array $params + * @param array $params * * @return string Built base string */ @@ -231,9 +298,9 @@ private function buildBaseString($baseURI, $method, $params) $return = array(); ksort($params); - foreach($params as $key=>$value) + foreach($params as $key => $value) { - $return[] = "$key=" . $value; + $return[] = rawurlencode($key) . '=' . rawurlencode($value); } return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); @@ -246,18 +313,45 @@ private function buildBaseString($baseURI, $method, $params) * * @return string $return Header used by cURL for request */ - private function buildAuthorizationHeader($oauth) + private function buildAuthorizationHeader(array $oauth) { $return = 'Authorization: OAuth '; $values = array(); foreach($oauth as $key => $value) { - $values[] = "$key=\"" . rawurlencode($value) . "\""; + if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature', + 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) { + $values[] = "$key=\"" . rawurlencode($value) . "\""; + } } $return .= implode(', ', $values); return $return; } + /** + * Helper method to perform our request + * + * @param string $url + * @param string $method + * @param string $data + * + * @throws \Exception + * + * @return string The json response from the server + */ + public function request($url, $method = 'get', $data = null) + { + if (strtolower($method) === 'get') + { + $this->setGetfield($data); + } + else + { + $this->setPostfields($data); + } + + return $this->buildOauth($url, $method)->performRequest(); + } } diff --git a/composer.json b/composer.json index 2032224..ff020a2 100644 --- a/composer.json +++ b/composer.json @@ -1,18 +1,28 @@ { - "name": "j7mbo/twitter-api-php", - "description": "Simple PHP Wrapper for Twitter API v1.1 calls", - "version": "0.1", - "type": "library", - "keywords": ["twitter", "PHP", "API"], - "homepage": "https://github.com/j7mbo/twitter-api-php", - "license": "GNU Public License", - "authors": [ - { - "name": "James Mallison", - "homepage": "https://github.com/j7mbo/twitter-api-php" + "require": { + "ext-curl": "*" + }, + "require-dev": { + "phpunit/phpunit": "4.5.1" + }, + "name": "j7mbo/twitter-api-php", + "description": "Simple PHP Wrapper for Twitter API v1.1 calls", + "version": "1.0.0", + "type": "library", + "keywords": [ + "twitter", + "PHP", + "API" + ], + "homepage": "https://github.com/j7mbo/twitter-api-php", + "license": "GNU Public License", + "authors": [ + { + "name": "James Mallison", + "homepage": "https://github.com/j7mbo/twitter-api-php" + } + ], + "autoload": { + "files": ["TwitterAPIExchange.php"] } - ], - "autoload": { - "files": ["TwitterAPIExchange.php"] - } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..ee5a42b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./test/ + + + \ No newline at end of file diff --git a/test/TwitterAPIExchangeTest.php b/test/TwitterAPIExchangeTest.php new file mode 100644 index 0000000..1edd300 --- /dev/null +++ b/test/TwitterAPIExchangeTest.php @@ -0,0 +1,257 @@ +getConstants() as $key => $value) + { + $settings[strtolower($key)] = $value; + } + + $this->exchange = new \TwitterAPIExchange($settings); + } + + /** + * GET statuses/mentions_timeline + * + * @see https://dev.twitter.com/rest/reference/get/statuses/mentions_timeline + */ + public function testStatusesMentionsTimeline() + { + $url = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json'; + $method = 'GET'; + $params = '?max_id=595150043381915648'; + + $data = $this->exchange->request($url, $method, $params); + $expected = "@j7php Test mention"; + + $this->assertContains($expected, $data); + } + + /** + * GET statuses/user_timeline + * + * @see https://dev.twitter.com/rest/reference/get/statuses/user_timeline + */ + public function testStatusesUserTimeline() + { + $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'; + $method = 'GET'; + $params = '?user_id=3232926711'; + + $data = $this->exchange->request($url, $method, $params); + $expected = "Test Tweet"; + + $this->assertContains($expected, $data); + } + + /** + * GET statuses/home_timeline + * + * @see https://dev.twitter.com/rest/reference/get/statuses/home_timeline + */ + public function testStatusesHomeTimeline() + { + $url = 'https://api.twitter.com/1.1/statuses/home_timeline.json'; + $method = 'GET'; + $params = '?user_id=3232926711'; + + $data = $this->exchange->request($url, $method, $params); + $expected = "Test Tweet"; + + $this->assertContains($expected, $data); + } + + /** + * GET statuses/retweets_of_me + * + * @see https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me + */ + public function testStatusesRetweetsOfMe() + { + $url = 'https://api.twitter.com/1.1/statuses/retweets_of_me.json'; + $method = 'GET'; + + $data = $this->exchange->request($url, $method); + $expected = 'travis CI and tests'; + + $this->assertContains($expected, $data); + } + + /** + * GET statuses/retweets/:id + * + * @see https://api.twitter.com/1.1/statuses/retweets/:id.json + */ + public function testStatusesRetweetsOfId() + { + $url = 'https://api.twitter.com/1.1/statuses/retweets/595155660494471168.json'; + $method = 'GET'; + + $data = $this->exchange->request($url, $method); + $expected = 'travis CI and tests'; + + $this->assertContains($expected, $data); + } + + /** + * GET Statuses/Show/:id + * + * @see https://dev.twitter.com/rest/reference/get/statuses/show/:id + */ + public function testStatusesShowId() + { + $url = 'https://api.twitter.com/1.1/statuses/show.json'; + $method = 'GET'; + $params = '?id=595155660494471168'; + + $data = $this->exchange->request($url, $method, $params); + $expected = 'travis CI and tests'; + + $this->assertContains($expected, $data); + } + + /** + * POST media/upload + * + * @see https://dev.twitter.com/rest/reference/post/media/upload + * + * @note Uploaded unattached media files will be available for attachment to a tweet for 60 minutes + */ + public function testMediaUpload() + { + $file = file_get_contents(__DIR__ . '/img.png'); + $data = base64_encode($file); + + $url = 'https://upload.twitter.com/1.1/media/upload.json'; + $method = 'POST'; + $params = array( + 'media_data' => $data + ); + + $data = $this->exchange->request($url, $method, $params); + $expected = 'image\/png'; + + $this->assertContains($expected, $data); + + /** Store the media id for later **/ + $data = @json_decode($data, true); + + $this->assertArrayHasKey('media_id', is_array($data) ? $data : array()); + + self::$mediaId = $data['media_id']; + } + + /** + * POST statuses/update + * + * @see https://dev.twitter.com/rest/reference/post/statuses/update + */ + public function testStatusesUpdate() + { + if (!self::$mediaId) + { + $this->fail('Cannot /update status because /upload failed'); + } + + $url = 'https://api.twitter.com/1.1/statuses/update.json'; + $method = 'POST'; + $params = array( + 'status' => 'TEST TWEET TO BE DELETED' . rand(), + 'media_ids' => self::$mediaId + ); + + $data = $this->exchange->request($url, $method, $params); + $expected = 'TEST TWEET TO BE DELETED'; + + $this->assertContains($expected, $data); + + /** Store the tweet id for testStatusesDestroy() **/ + $data = @json_decode($data, true); + + $this->assertArrayHasKey('id_str', is_array($data) ? $data : array()); + + self::$tweetId = $data['id_str']; + + /** We've done this now, yay **/ + self::$mediaId = null; + } + + /** + * POST statuses/destroy/:id + * + * @see https://dev.twitter.com/rest/reference/post/statuses/destroy/:id + */ + public function testStatusesDestroy() + { + if (!self::$tweetId) + { + $this->fail('Cannot /destroy status because /update failed'); + } + + $url = sprintf('https://api.twitter.com/1.1/statuses/destroy/%d.json', self::$tweetId); + $method = 'POST'; + $params = array( + 'id' => self::$tweetId + ); + + $data = $this->exchange->request($url, $method, $params); + $expected = 'TEST TWEET TO BE DELETED'; + + $this->assertContains($expected, $data); + + /** We've done this now, yay **/ + self::$tweetId = null; + } +} \ No newline at end of file diff --git a/test/img.png b/test/img.png new file mode 100644 index 0000000..85f8ce3 Binary files /dev/null and b/test/img.png differ