diff --git a/examples/custom-request.php b/examples/custom-request.php
index d4c7778..36d77e5 100644
--- a/examples/custom-request.php
+++ b/examples/custom-request.php
@@ -16,11 +16,11 @@
Twitter retweets of me
diff --git a/examples/load.php b/examples/load.php
index a223e3a..6a1fa39 100644
--- a/examples/load.php
+++ b/examples/load.php
@@ -19,11 +19,11 @@
Twitter timeline demo
diff --git a/examples/search.php b/examples/search.php
index 6488efb..49344ad 100644
--- a/examples/search.php
+++ b/examples/search.php
@@ -16,11 +16,11 @@
Twitter search demo
diff --git a/readme.md b/readme.md
index 9c09818..85009c3 100644
--- a/readme.md
+++ b/readme.md
@@ -6,8 +6,6 @@
Twitter for PHP is a very small and easy-to-use library for sending
messages to Twitter and receiving status updates.
-If you like this, **[please make a donation now](https://nette.org/make-donation?to=twitter-php)**. Thank you!
-
It requires PHP 5.4 or newer with CURL extension and is licensed under the New BSD License.
You can obtain the latest version from our [GitHub repository](https://github.com/dg/twitter-php)
or install it via Composer:
@@ -15,6 +13,16 @@ or install it via Composer:
composer require dg/twitter-php
+[Support Me](https://github.com/sponsors/dg)
+--------------------------------------------
+
+Do you like Nette DI? Are you looking forward to the new features?
+
+[](https://github.com/sponsors/dg)
+
+Thank you!
+
+
Usage
-----
Sign in to the https://twitter.com and register an application from the https://apps.twitter.com page. Remember
@@ -108,7 +116,7 @@ if (!$twitter->authenticate()) {
Other commands
--------------
-You can use all commands defined by [Twitter API 1.1](https://dev.twitter.com/rest/public).
+You can use all commands defined by [Twitter API](https://dev.twitter.com/rest/public).
For example [GET statuses/retweets_of_me](https://dev.twitter.com/rest/reference/get/statuses/retweets_of_me)
returns the array of most recent tweets authored by the authenticating user:
@@ -116,6 +124,12 @@ returns the array of most recent tweets authored by the authenticating user:
$statuses = $twitter->request('statuses/retweets_of_me', 'GET', ['count' => 20]);
```
+You can also specify which API version to use with the API_*_SUFFIX constants:
+
+```php
+$statuses = $twitter->request('tweets', 'GET', [], [], Twitter::API_2_SUFFIX);
+```
+
Changelog
---------
v4.1 (11/2019)
diff --git a/src/OAuth.php b/src/OAuth.php
index 38d3505..db096e2 100644
--- a/src/OAuth.php
+++ b/src/OAuth.php
@@ -302,12 +302,18 @@ public function __construct(string $http_method, string $http_url, array $parame
/**
* attempt to build up a request from what was passed to the server
*/
- public static function from_request(string $http_method = null, string $http_url = null, array $parameters = null): self
+ public static function from_request(
+ string $http_method = null,
+ string $http_url = null,
+ array $parameters = null
+ ): self
{
$scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on')
? 'http'
: 'https';
- $http_url = ($http_url) ? $http_url : $scheme .
+ $http_url = ($http_url)
+ ? $http_url
+ : $scheme .
'://' . $_SERVER['HTTP_HOST'] .
':' .
$_SERVER['SERVER_PORT'] .
@@ -339,7 +345,10 @@ public static function from_request(string $http_method = null, string $http_url
// We have a Authorization-header with OAuth data. Parse the header
// and add those overriding any duplicates from GET or POST
- if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') {
+ if (
+ isset($request_headers['Authorization'])
+ && substr($request_headers['Authorization'], 0, 6) == 'OAuth '
+ ) {
$header_parameters = Util::split_header(
$request_headers['Authorization']
);
@@ -354,7 +363,13 @@ public static function from_request(string $http_method = null, string $http_url
/**
* pretty much a helper function to set up the request
*/
- public static function from_consumer_and_token(Consumer $consumer, ?Token $token, string $http_method, string $http_url, array $parameters = null): self
+ public static function from_consumer_and_token(
+ Consumer $consumer,
+ ?Token $token,
+ string $http_method,
+ string $http_url,
+ array $parameters = null
+ ): self
{
$parameters = $parameters ?: [];
$defaults = [
@@ -392,7 +407,7 @@ public function set_parameter(string $name, $value, bool $allow_duplicates = tru
public function get_parameter(string $name)
{
- return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+ return $this->parameters[$name] ?? null;
}
@@ -465,7 +480,9 @@ public function get_normalized_http_url(): string
$parts = parse_url($this->http_url);
$scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
- $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80');
+ $port = (isset($parts['port']))
+ ? $parts['port']
+ : (($scheme == 'https') ? '443' : '80');
$host = (isset($parts['host'])) ? $parts['host'] : '';
$path = (isset($parts['path'])) ? $parts['path'] : '';
@@ -581,7 +598,7 @@ class Util
public static function urlencode_rfc3986($input)
{
if (is_array($input)) {
- return array_map([__CLASS__, 'urlencode_rfc3986'], $input);
+ return array_map([self::class, 'urlencode_rfc3986'], $input);
} elseif (is_scalar($input)) {
return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode((string) $input)));
} else {
diff --git a/src/Twitter.php b/src/Twitter.php
index 952e568..a78ceed 100644
--- a/src/Twitter.php
+++ b/src/Twitter.php
@@ -28,7 +28,10 @@ class Twitter
public const REPLIES = 3;
public const RETWEETS = 128; // include retweets?
- private const API_URL = 'https://api.twitter.com/1.1/';
+ public const API_1_SUFFIX = "1.1";
+ public const API_2_SUFFIX = "2";
+
+ private const API_URL = 'https://api.twitter.com/';
/** @var int */
public static $cacheExpire = '30 minutes';
@@ -54,8 +57,12 @@ class Twitter
* Creates object using consumer and access keys.
* @throws Exception when CURL extension is not loaded
*/
- public function __construct(string $consumerKey, string $consumerSecret, string $accessToken = null, string $accessTokenSecret = null)
- {
+ public function __construct(
+ string $consumerKey,
+ string $consumerSecret,
+ string $accessToken = null,
+ string $accessTokenSecret = null
+ ) {
if (!extension_loaded('curl')) {
throw new Exception('PHP extension CURL is not loaded.');
}
@@ -196,7 +203,12 @@ public function loadUserInfoById(string $id): stdClass
* https://dev.twitter.com/rest/reference/get/followers/ids
* @throws Exception
*/
- public function loadUserFollowers(string $username, int $count = 5000, int $cursor = -1, $cacheExpiry = null): stdClass
+ public function loadUserFollowers(
+ string $username,
+ int $count = 5000,
+ int $cursor = -1,
+ $cacheExpiry = null
+ ): stdClass
{
return $this->cachedRequest('followers/ids', [
'screen_name' => $username,
@@ -211,7 +223,12 @@ public function loadUserFollowers(string $username, int $count = 5000, int $curs
* https://dev.twitter.com/rest/reference/get/followers/list
* @throws Exception
*/
- public function loadUserFollowersList(string $username, int $count = 200, int $cursor = -1, $cacheExpiry = null): stdClass
+ public function loadUserFollowersList(
+ string $username,
+ int $count = 200,
+ int $cursor = -1,
+ $cacheExpiry = null
+ ): stdClass
{
return $this->cachedRequest('followers/list', [
'screen_name' => $username,
@@ -228,7 +245,7 @@ public function loadUserFollowersList(string $username, int $count = 200, int $c
*/
public function destroy($id)
{
- $res = $this->request("statuses/destroy/$id", 'POST');
+ $res = $this->request("statuses/destroy/$id", 'POST', ['id' => $id]);
return $res->id ?: false;
}
@@ -270,18 +287,62 @@ public function getTrends(int $WOEID): array
/**
- * Process HTTP request.
- * @param string $method GET|POST|JSONPOST|DELETE
- * @return mixed
+ * Generates an API url, requires at minimum 2 parts (version & path).
+ *
+ * @param string ...$parts Collection of URL parts to combine
* @throws Exception
+ * @return string
+ */
+ protected static function makeApiURL(string ...$parts) {
+ $url = [];
+ $partsCount = count($parts);
+
+ if ($partsCount < 1) {
+ throw new Exception("Invalid API URL components provided. Must have at least 2 parts (version & path)");
+ }
+
+ $url[] = substr(self::API_URL, 0, strlen(self::API_URL) - 1);
+
+ for ($i = 0; $i < $partsCount; $i++) {
+ $part = $parts[$i];
+ $partLen = strlen($part);
+
+ if ($part[$partLen - 1] == '/') {
+ $part = substr($part, 0, $partLen - 1);
+ }
+
+ if ($part[0] == '/') {
+ $part = substr($part, 1);
+ }
+
+ $url[] = $part;
+ }
+
+ return implode('/', $url);
+ }
+
+
+ /**
+ * Process HTTP request. If $resource contains only endpoint path (no http://|https://), API_URL will be prefixed
+ * onto resource path. If $apiSuffix is '1.1' (default), resource will have '.json' added as a suffix if '.'
+ * character not found.
+ *
+ * @param string $resource API endpoint
+ * @param string $method GET|POST|JSONPOST|DELETE
+ * @param array $data Optional query/body data
+ * @param array $files Optional file data
+ * @param string $apiSuffix Optional API version suffix (1.1 by default)
+ * @throws Exception|\DG\Twitter\OAuth\Exception
+ * @return mixed
*/
- public function request(string $resource, string $method, array $data = [], array $files = [])
+ public function request(string $resource, string $method, array $data = [], array $files = [], string $apiSuffix = self::API_1_SUFFIX)
{
if (!strpos($resource, '://')) {
- if (!strpos($resource, '.')) {
+ if ($apiSuffix == self::API_1_SUFFIX && !strpos($resource, '.')) {
$resource .= '.json';
}
- $resource = self::API_URL . $resource;
+
+ $resource = static::makeApiURL($apiSuffix, $resource);
}
foreach ($data as $key => $val) {
@@ -294,6 +355,7 @@ public function request(string $resource, string $method, array $data = [], arra
if (!is_file($file)) {
throw new Exception("Cannot read the file $file. Check if file exists on disk and check its permissions.");
}
+
$data[$key] = new \CURLFile($file);
}
@@ -303,7 +365,6 @@ public function request(string $resource, string $method, array $data = [], arra
$method = 'POST';
$data = json_encode($data);
$headers[] = 'Content-Type: application/json';
-
} elseif (($method === 'GET' || $method === 'DELETE') && $data) {
$resource .= '?' . http_build_query($data, '', '&');
}
@@ -334,12 +395,16 @@ public function request(string $resource, string $method, array $data = [], arra
$curl = curl_init();
curl_setopt_array($curl, $options);
$result = curl_exec($curl);
+
if (curl_errno($curl)) {
throw new Exception('Server error: ' . curl_error($curl));
}
- if (strpos(curl_getinfo($curl, CURLINFO_CONTENT_TYPE), 'application/json') !== false) {
+ $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE);
+
+ if ($contentType === false || strpos($contentType, 'application/json') !== false) {
$payload = @json_decode($result, false, 128, JSON_BIGINT_AS_STRING); // intentionally @
+
if ($payload === false) {
throw new Exception('Invalid server response');
}
@@ -347,9 +412,8 @@ public function request(string $resource, string $method, array $data = [], arra
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ($code >= 400) {
- throw new Exception(isset($payload->errors[0]->message)
- ? $payload->errors[0]->message
- : "Server error #$code with answer $result",
+ throw new Exception(
+ $payload->errors[0]->message ?? "Server error #$code with answer $result",
$code
);
} elseif ($code === 204) {
@@ -379,7 +443,9 @@ public function cachedRequest(string $resource, array $data = [], $cacheExpire =
. '.json';
$cache = @json_decode((string) @file_get_contents($cacheFile)); // intentionally @
- $expiration = is_string($cacheExpire) ? strtotime($cacheExpire) - time() : $cacheExpire;
+ $expiration = is_string($cacheExpire)
+ ? strtotime($cacheExpire) - time()
+ : $cacheExpire;
if ($cache && @filemtime($cacheFile) + $expiration > time()) { // intentionally @
return $cache;
}
@@ -424,7 +490,7 @@ public static function clickable(stdClass $status): string
}
krsort($all);
- $s = isset($status->full_text) ? $status->full_text : $status->text;
+ $s = $status->full_text ?? $status->text;
foreach ($all as $pos => $item) {
$s = iconv_substr($s, 0, $pos, 'UTF-8')
. '' . htmlspecialchars($item[1]) . ''