From 57ea69588ae3a36de5d9e0c145c6cbd679fea0d0 Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Fri, 24 Apr 2015 19:07:20 +0100 Subject: [PATCH 01/18] Encode the data as application/x-www-form-urlencoded --- TwitterAPIExchange.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index 7832725..1ca69a4 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -199,7 +199,7 @@ public function performRequest($return = true) if (!is_null($postfields)) { - $options[CURLOPT_POSTFIELDS] = $postfields; + $options[CURLOPT_POSTFIELDS] = http_build_query($postfields); } else { From 299c3f934d54c7874fb3bc77d3231f7fdbd22df9 Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Fri, 24 Apr 2015 19:11:55 +0100 Subject: [PATCH 02/18] Use POST fields for building oAuth base string --- TwitterAPIExchange.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index 1ca69a4..c83f5ac 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -159,6 +159,14 @@ public function buildOauth($url, $requestMethod) } } + $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)); From 83c9efdda7fb8dd2f13a2239083cc81c71e878a9 Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Sat, 25 Apr 2015 06:54:56 +0100 Subject: [PATCH 03/18] Preserve backwards compatibility by building signature if it does not exist when calling performRequest() --- TwitterAPIExchange.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index c83f5ac..a8f079e 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -21,6 +21,7 @@ class TwitterAPIExchange private $getfield; protected $oauth; public $url; + public $requestMethod; /** * Create the API access object. Requires an array of settings:: @@ -173,6 +174,7 @@ public function buildOauth($url, $requestMethod) $oauth['oauth_signature'] = $oauth_signature; $this->url = $url; + $this->requestMethod = $requestMethod; $this->oauth = $oauth; return $this; @@ -192,6 +194,10 @@ public function performRequest($return = true) throw new Exception('performRequest parameter must be true or false'); } + if (!isset($this->oauth['oauth_signature'])) { + $this->buildOauth($this->url, $this->requestMethod); + } + $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); $getfield = $this->getGetfield(); From b91a4af15b73fd359b8a2f3b9f0a1574fc836848 Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Sat, 25 Apr 2015 07:02:07 +0100 Subject: [PATCH 04/18] Move rebuild oAuth check to setPostFields method --- TwitterAPIExchange.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index a8f079e..bb92d4d 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -73,6 +73,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; } @@ -194,10 +199,6 @@ public function performRequest($return = true) throw new Exception('performRequest parameter must be true or false'); } - if (!isset($this->oauth['oauth_signature'])) { - $this->buildOauth($this->url, $this->requestMethod); - } - $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); $getfield = $this->getGetfield(); From 58d8ec3d5bb02e6e6c3d1cdfa8259f9d64caffaf Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Mon, 27 Apr 2015 16:03:41 +0100 Subject: [PATCH 05/18] URL encode key and value of parameters prior to creating base string --- TwitterAPIExchange.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index bb92d4d..4b56974 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -248,7 +248,7 @@ private function buildBaseString($baseURI, $method, $params) foreach($params as $key=>$value) { - $return[] = "$key=" . $value; + $return[] = rawurlencode($key) . '=' . rawurlencode($value); } return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); From 2dbf9f4124cddd87d097d44c8d525a2ec29a821c Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:14:57 +0100 Subject: [PATCH 06/18] Added check for null getfield Added shortcut request method Added phpdocs --- TwitterAPIExchange.php | 112 ++++++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index 4b56974..41e0e55 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -13,14 +13,49 @@ */ 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; /** @@ -28,7 +63,9 @@ class TwitterAPIExchange * 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) @@ -51,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) @@ -85,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 */ @@ -127,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) @@ -139,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', @@ -158,10 +202,16 @@ 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]; + } } } @@ -188,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. */ @@ -198,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, @@ -229,7 +281,7 @@ public function performRequest($return = true) $json = curl_exec($feed); curl_close($feed); - if ($return) { return $json; } + return $json; } /** @@ -237,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 */ @@ -246,7 +298,7 @@ private function buildBaseString($baseURI, $method, $params) $return = array(); ksort($params); - foreach($params as $key=>$value) + foreach($params as $key => $value) { $return[] = rawurlencode($key) . '=' . rawurlencode($value); } @@ -261,7 +313,7 @@ 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(); @@ -275,4 +327,28 @@ private function buildAuthorizationHeader($oauth) 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(); + } } From 6f18415cf3852ec0a4b7045cafb12a0591c322b9 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:15:34 +0100 Subject: [PATCH 07/18] Update README, .gitignore --- .gitignore | 3 +++ README.md | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .gitignore 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/README.md b/README.md index 6db3c5c..939aeb7 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ Simple PHP Wrapper for Twitter API v1.1 calls **[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": { From 72b7ce7c958a6f1c81bf53f03c704600e0eaea78 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:15:49 +0100 Subject: [PATCH 08/18] Added tests --- .travis.yml | 12 ++ composer.json | 40 +++-- phpunit.xml | 18 +++ test/TwitterAPIExchangeTest.php | 273 ++++++++++++++++++++++++++++++++ test/img.png | Bin 0 -> 19332 bytes 5 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 .travis.yml create mode 100644 phpunit.xml create mode 100644 test/TwitterAPIExchangeTest.php create mode 100644 test/img.png diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f6c07ad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php +php: + - "5.3.4" + +before_install: + - composer self-update + +before_script: + - composer install + +script: + - phpunit --configuration phpunit.xml \ No newline at end of file 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..1eb2cf1 --- /dev/null +++ b/test/TwitterAPIExchangeTest.php @@ -0,0 +1,273 @@ +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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + /** + *========= + * ========= + * This test is currently failing because media/upload doesn't work yet + * ========= + *========= + */ + $file = file_get_contents(__DIR__ . '/img.png'); + $data = base64_encode($file); + + $url = 'https://upload.twitter.com/1.1/media/upload.json'; + $method = 'POST'; + $params = [ + '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 : []); + + self::$mediaId = $data['media_id']; + + var_dump(self::$mediaId); + } + + /** + * POST statuses/update + * + * @see https://dev.twitter.com/rest/reference/post/statuses/update + */ + public function testStatusesUpdate() + { + $this->markTestSkipped(); + if (!self::$mediaId) + { + $this->fail('Cannot /update status because /upload failed'); + } + + $url = 'https://api.twitter.com/1.1/statuses/update.json'; + $method = 'POST'; + $params = [ + 'status' => 'TEST TWEET TO BE DELETED' . rand() + ]; + + $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 : []); + + 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/%3Aid + */ + public function testStatusesDestroy() + { + $this->markTestSkipped(); + 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 = [ + '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 0000000000000000000000000000000000000000..85f8ce31a7c5b4f801e85c38a8746cbd804b1f7e GIT binary patch literal 19332 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 literal 0 HcmV?d00001 From ae7e64440a4dc10ae9814e29749d7167b9b20784 Mon Sep 17 00:00:00 2001 From: Glen Scott Date: Mon, 4 May 2015 13:58:56 +0100 Subject: [PATCH 09/18] Only use oAuth parameters in Authorization header --- TwitterAPIExchange.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index 4b56974..f547441 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -268,7 +268,10 @@ private function buildAuthorizationHeader($oauth) 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); From 44c2a1cd88800015584af2a6f903c9247e7b3ed3 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:14:57 +0100 Subject: [PATCH 10/18] Added check for null getfield Added shortcut request method Added phpdocs --- TwitterAPIExchange.php | 112 ++++++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/TwitterAPIExchange.php b/TwitterAPIExchange.php index f547441..71f8f59 100755 --- a/TwitterAPIExchange.php +++ b/TwitterAPIExchange.php @@ -13,14 +13,49 @@ */ 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; /** @@ -28,7 +63,9 @@ class TwitterAPIExchange * 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) @@ -51,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) @@ -85,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 */ @@ -127,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) @@ -139,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', @@ -158,10 +202,16 @@ 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]; + } } } @@ -188,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. */ @@ -198,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, @@ -229,7 +281,7 @@ public function performRequest($return = true) $json = curl_exec($feed); curl_close($feed); - if ($return) { return $json; } + return $json; } /** @@ -237,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 */ @@ -246,7 +298,7 @@ private function buildBaseString($baseURI, $method, $params) $return = array(); ksort($params); - foreach($params as $key=>$value) + foreach($params as $key => $value) { $return[] = rawurlencode($key) . '=' . rawurlencode($value); } @@ -261,7 +313,7 @@ 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(); @@ -278,4 +330,28 @@ private function buildAuthorizationHeader($oauth) 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(); + } } From 70527bdc197426aa9dc8450d215a989bd1025327 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:15:34 +0100 Subject: [PATCH 11/18] Update README, .gitignore --- .gitignore | 3 +++ README.md | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .gitignore 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/README.md b/README.md index 6db3c5c..939aeb7 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ Simple PHP Wrapper for Twitter API v1.1 calls **[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": { From 9e582e30d920e6a1fd99c92e1d1fe918c94a3f80 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 13:15:49 +0100 Subject: [PATCH 12/18] Added tests --- .travis.yml | 12 ++ composer.json | 40 +++-- phpunit.xml | 18 +++ test/TwitterAPIExchangeTest.php | 273 ++++++++++++++++++++++++++++++++ test/img.png | Bin 0 -> 19332 bytes 5 files changed, 328 insertions(+), 15 deletions(-) create mode 100644 .travis.yml create mode 100644 phpunit.xml create mode 100644 test/TwitterAPIExchangeTest.php create mode 100644 test/img.png diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f6c07ad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php +php: + - "5.3.4" + +before_install: + - composer self-update + +before_script: + - composer install + +script: + - phpunit --configuration phpunit.xml \ No newline at end of file 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..1eb2cf1 --- /dev/null +++ b/test/TwitterAPIExchangeTest.php @@ -0,0 +1,273 @@ +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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + $this->markTestSkipped(); + $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() + { + /** + *========= + * ========= + * This test is currently failing because media/upload doesn't work yet + * ========= + *========= + */ + $file = file_get_contents(__DIR__ . '/img.png'); + $data = base64_encode($file); + + $url = 'https://upload.twitter.com/1.1/media/upload.json'; + $method = 'POST'; + $params = [ + '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 : []); + + self::$mediaId = $data['media_id']; + + var_dump(self::$mediaId); + } + + /** + * POST statuses/update + * + * @see https://dev.twitter.com/rest/reference/post/statuses/update + */ + public function testStatusesUpdate() + { + $this->markTestSkipped(); + if (!self::$mediaId) + { + $this->fail('Cannot /update status because /upload failed'); + } + + $url = 'https://api.twitter.com/1.1/statuses/update.json'; + $method = 'POST'; + $params = [ + 'status' => 'TEST TWEET TO BE DELETED' . rand() + ]; + + $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 : []); + + 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/%3Aid + */ + public function testStatusesDestroy() + { + $this->markTestSkipped(); + 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 = [ + '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 0000000000000000000000000000000000000000..85f8ce31a7c5b4f801e85c38a8746cbd804b1f7e GIT binary patch literal 19332 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 literal 0 HcmV?d00001 From 52083b50ddb0f2ac99427e12be7f3fd32f4b8cad Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 14:47:16 +0100 Subject: [PATCH 13/18] Updated tests and fixed media/upload --- test/TwitterAPIExchangeTest.php | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/test/TwitterAPIExchangeTest.php b/test/TwitterAPIExchangeTest.php index 1eb2cf1..e287f53 100644 --- a/test/TwitterAPIExchangeTest.php +++ b/test/TwitterAPIExchangeTest.php @@ -69,7 +69,6 @@ public function setUp() */ public function testStatusesMentionsTimeline() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/mentions_timeline.json'; $method = 'GET'; $params = '?max_id=595150043381915648'; @@ -87,7 +86,6 @@ public function testStatusesMentionsTimeline() */ public function testStatusesUserTimeline() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/user_timeline.json'; $method = 'GET'; $params = '?user_id=3232926711'; @@ -105,7 +103,6 @@ public function testStatusesUserTimeline() */ public function testStatusesHomeTimeline() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/home_timeline.json'; $method = 'GET'; $params = '?user_id=3232926711'; @@ -123,7 +120,6 @@ public function testStatusesHomeTimeline() */ public function testStatusesRetweetsOfMe() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/retweets_of_me.json'; $method = 'GET'; @@ -140,7 +136,6 @@ public function testStatusesRetweetsOfMe() */ public function testStatusesRetweetsOfId() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/retweets/595155660494471168.json'; $method = 'GET'; @@ -157,7 +152,6 @@ public function testStatusesRetweetsOfId() */ public function testStatusesShowId() { - $this->markTestSkipped(); $url = 'https://api.twitter.com/1.1/statuses/show.json'; $method = 'GET'; $params = '?id=595155660494471168'; @@ -177,13 +171,6 @@ public function testStatusesShowId() */ public function testMediaUpload() { - /** - *========= - * ========= - * This test is currently failing because media/upload doesn't work yet - * ========= - *========= - */ $file = file_get_contents(__DIR__ . '/img.png'); $data = base64_encode($file); @@ -194,7 +181,7 @@ public function testMediaUpload() ]; $data = $this->exchange->request($url, $method, $params); - $expected = 'image/png'; + $expected = 'image\/png'; $this->assertContains($expected, $data); @@ -204,8 +191,6 @@ public function testMediaUpload() $this->assertArrayHasKey('media_id', is_array($data) ? $data : []); self::$mediaId = $data['media_id']; - - var_dump(self::$mediaId); } /** @@ -215,7 +200,6 @@ public function testMediaUpload() */ public function testStatusesUpdate() { - $this->markTestSkipped(); if (!self::$mediaId) { $this->fail('Cannot /update status because /upload failed'); @@ -224,7 +208,8 @@ public function testStatusesUpdate() $url = 'https://api.twitter.com/1.1/statuses/update.json'; $method = 'POST'; $params = [ - 'status' => 'TEST TWEET TO BE DELETED' . rand() + 'status' => 'TEST TWEET TO BE DELETED' . rand(), + 'media_ids' => self::$mediaId ]; $data = $this->exchange->request($url, $method, $params); @@ -246,11 +231,10 @@ public function testStatusesUpdate() /** * POST statuses/destroy/:id * - * @see https://dev.twitter.com/rest/reference/post/statuses/destroy/%3Aid + * @see https://dev.twitter.com/rest/reference/post/statuses/destroy/:id */ public function testStatusesDestroy() { - $this->markTestSkipped(); if (!self::$tweetId) { $this->fail('Cannot /destroy status because /update failed'); From 43a6edbd1e8f21156279f09ab36f85069dc48cec Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 14:57:50 +0100 Subject: [PATCH 14/18] Added travis badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 939aeb7..e059a43 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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)** || From 8a50239036b5ca2914647feb051f784630e2b191 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 14:58:47 +0100 Subject: [PATCH 15/18] Fixed markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e059a43..3fead7f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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) +[![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)** || From 343738fec6f607bb41ee4a634dc1256bb8479cbd Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 15:00:08 +0100 Subject: [PATCH 16/18] Fixed .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f6c07ad..abac424 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php php: - - "5.3.4" + - "5.3" before_install: - composer self-update From 714e5e152d750308dbd2bf85a1af20d5146562f5 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 15:03:41 +0100 Subject: [PATCH 17/18] 5.3 compatibility --- test/TwitterAPIExchangeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/TwitterAPIExchangeTest.php b/test/TwitterAPIExchangeTest.php index e287f53..fd18500 100644 --- a/test/TwitterAPIExchangeTest.php +++ b/test/TwitterAPIExchangeTest.php @@ -49,7 +49,7 @@ class TwitterAPIExchangeTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - $settings = []; + $settings = array(); /** Because I'm lazy... **/ $reflector = new \ReflectionClass($this); From 90ccc1d560a67a56072f3c18607de67d03c58139 Mon Sep 17 00:00:00 2001 From: JamesMallison Date: Mon, 4 May 2015 15:14:40 +0100 Subject: [PATCH 18/18] 5.3 compatibility --- test/TwitterAPIExchangeTest.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/TwitterAPIExchangeTest.php b/test/TwitterAPIExchangeTest.php index fd18500..1edd300 100644 --- a/test/TwitterAPIExchangeTest.php +++ b/test/TwitterAPIExchangeTest.php @@ -176,9 +176,9 @@ public function testMediaUpload() $url = 'https://upload.twitter.com/1.1/media/upload.json'; $method = 'POST'; - $params = [ + $params = array( 'media_data' => $data - ]; + ); $data = $this->exchange->request($url, $method, $params); $expected = 'image\/png'; @@ -188,7 +188,7 @@ public function testMediaUpload() /** Store the media id for later **/ $data = @json_decode($data, true); - $this->assertArrayHasKey('media_id', is_array($data) ? $data : []); + $this->assertArrayHasKey('media_id', is_array($data) ? $data : array()); self::$mediaId = $data['media_id']; } @@ -207,10 +207,10 @@ public function testStatusesUpdate() $url = 'https://api.twitter.com/1.1/statuses/update.json'; $method = 'POST'; - $params = [ + $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'; @@ -220,7 +220,7 @@ public function testStatusesUpdate() /** Store the tweet id for testStatusesDestroy() **/ $data = @json_decode($data, true); - $this->assertArrayHasKey('id_str', is_array($data) ? $data : []); + $this->assertArrayHasKey('id_str', is_array($data) ? $data : array()); self::$tweetId = $data['id_str']; @@ -242,9 +242,9 @@ public function testStatusesDestroy() $url = sprintf('https://api.twitter.com/1.1/statuses/destroy/%d.json', self::$tweetId); $method = 'POST'; - $params = [ + $params = array( 'id' => self::$tweetId - ]; + ); $data = $this->exchange->request($url, $method, $params); $expected = 'TEST TWEET TO BE DELETED';