-
Notifications
You must be signed in to change notification settings - Fork 23
Allows the *Lookup
methods to accept arrays of identifiers or screen names
#45
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
|
||
namespace ZendService\Twitter; | ||
|
||
use Closure; | ||
use Traversable; | ||
use ZendOAuth as OAuth; | ||
use Zend\Http; | ||
|
@@ -803,14 +804,21 @@ public function friendshipsCreate($id, array $params = []) : Response | |
* | ||
* Returns the next cursor if there are more to be returned. | ||
* | ||
* @param int|string $id | ||
* $id may be one of any of the following: | ||
* | ||
* - a single user identifier | ||
* - a single screen name | ||
* - a list of user identifiers | ||
* - a list of screen names | ||
* | ||
* @param int|string|array $id | ||
* @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | ||
* @throws Exception\DomainException if unable to decode JSON payload | ||
*/ | ||
public function friendshipsLookup($id, array $params = []) : Response | ||
{ | ||
$path = 'friendships/lookup'; | ||
$params = $this->createUserParameter($id, $params); | ||
$params = $this->createUserListParameter($id, $params, __METHOD__); | ||
return $this->get($path, $params); | ||
} | ||
|
||
|
@@ -1335,15 +1343,21 @@ public function statusesUserTimeline(array $options = []) : Response | |
* | ||
* This is the most effecient way of gathering bulk user data. | ||
* | ||
* @param int|string $id | ||
* $id may be one of any of the following: | ||
* | ||
* - a single user identifier | ||
* - a single screen name | ||
* - a list of user identifiers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above |
||
* - a list of screen names | ||
* | ||
* @param int|string|array $id | ||
* @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | ||
* @throws Exception\DomainException if unable to decode JSON payload | ||
*/ | ||
public function usersLookup($id, array $params = []) : Response | ||
{ | ||
$path = 'users/lookup'; | ||
// $params = $this->createUserParameter($id, $params); | ||
$params['user_id'] = $id; | ||
$params = $this->createUserListParameter($id, $params, __METHOD__); | ||
return $this->post($path, $params); | ||
} | ||
|
||
|
@@ -1446,16 +1460,17 @@ public function init(string $path, Http\Client $client) : void | |
* returned verbatim. Otherwise, a zero is returned. | ||
* | ||
* @param string|int $int | ||
* @return string|int | ||
*/ | ||
protected function validInteger($int) | ||
protected function validInteger($int) : int | ||
{ | ||
if (is_int($int)) { | ||
if (is_int($int) && $int > -1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I realized in reviewing tests that negative values aren't allowed, so I added this additional condition. |
||
return $int; | ||
} | ||
|
||
if (is_string($int) && preg_match('/^(\d+)$/', $int)) { | ||
return $int; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
|
@@ -1467,12 +1482,11 @@ protected function validInteger($int) | |
protected function validateScreenName(string $name) : string | ||
{ | ||
if (! is_string($name) || ! preg_match('/^[a-zA-Z0-9_]{1,15}$/', $name)) { | ||
throw new Exception\InvalidArgumentException( | ||
'Screen name, "' | ||
. $name | ||
. '" should only contain alphanumeric characters and' | ||
. ' underscores, and not exceed 15 characters.' | ||
); | ||
throw new Exception\InvalidArgumentException(sprintf( | ||
'Screen name, "%s" should only contain alphanumeric characters and' | ||
. ' underscores, and not exceed 15 characters.', | ||
$name | ||
)); | ||
} | ||
return $name; | ||
} | ||
|
@@ -1513,15 +1527,106 @@ protected function performPost(string $method, $data, Http\Client $client) : Htt | |
* | ||
* @param int|string $id | ||
* @param array $params | ||
* @throws Exception\InvalidArgumentException if the value is neither an integer nor a string | ||
*/ | ||
protected function createUserParameter($id, array $params) : array | ||
{ | ||
if ($this->validInteger($id)) { | ||
if (! is_string($id) && ! is_int($id)) { | ||
throw new Exception\InvalidArgumentException(sprintf( | ||
'$id must be an integer or a string, received %s', | ||
is_object($id) ? get_class($id) : gettype($id) | ||
)); | ||
} | ||
|
||
if (0 !== $this->validInteger($id)) { | ||
$params['user_id'] = $id; | ||
return $params; | ||
} | ||
|
||
if (! is_string($id)) { | ||
throw new Exception\InvalidArgumentException(sprintf( | ||
'$id must be an integer or a string, received %s', | ||
gettype($id) | ||
)); | ||
} | ||
|
||
$params['screen_name'] = $this->validateScreenName($id); | ||
return $params; | ||
} | ||
|
||
/** | ||
* Prepares a list of identifiers for use with endpoints accepting lists of users. | ||
* | ||
* The various `lookup` endpoints allow passing either: | ||
* | ||
* - a single user identifier | ||
* - a single screen name | ||
* - a list of user identifiers | ||
* - a list of screen names | ||
* | ||
* This method checks for each of these conditions. For scalar $ids, it | ||
* proxies to {@link createUserParameter}. Otherwise, it checks to ensure | ||
* that all values are of the same type. For identifiers, it then | ||
* concatenates the values and returns them in the `user_id` parameter. | ||
* For screen names, it validates them first, before concatenating and | ||
* returning them via the `screen_name` parameter. | ||
* | ||
* @param int|string|array $ids | ||
* @throws Exception\InvalidArgumentException for a non-int/string/array $ids value. | ||
* @throws Exception\InvalidArgumentException if an array of $ids exceeds 100 items. | ||
* @throws Exception\InvalidArgumentException if an array of $ids are not all of the same type. | ||
* @throws Exception\InvalidArgumentException if any screen name element is invalid. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a potential extension point for developers extending the class, as there are other methods in the Twitter API that behave similarly; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They look ok to me. Once they are implemented (and the other PR as well, I'll plug it into my projects and see if anything breaks. :) Cheers! |
||
protected function createUserListParameter($ids, array $params, string $context) : array | ||
{ | ||
if (! is_array($ids)) { | ||
return $this->createUserParameter($ids, $params); | ||
} | ||
|
||
if (100 < count($ids)) { | ||
throw new Exception\InvalidArgumentException(sprintf( | ||
'Lists of identifier(s) or screen name(s) provided for %s; ' | ||
. 'must contain no more than 100 items. ' | ||
. 'Received %d', | ||
$context, | ||
count($ids) | ||
)); | ||
} | ||
|
||
$detectedType = array_reduce($ids, function ($detectedType, $id) { | ||
if (false === $detectedType) { | ||
return $detectedType; | ||
} | ||
|
||
$idType = 0 !== $this->validInteger($id) | ||
? 'user_id' | ||
: 'screen_name'; | ||
|
||
if (null === $detectedType) { | ||
return $idType; | ||
} | ||
|
||
return $detectedType === $idType | ||
? $detectedType | ||
: false; | ||
}, null); | ||
|
||
if (false === $detectedType) { | ||
throw new Exception\InvalidArgumentException(sprintf( | ||
'Invalid identifier(s) or screen name(s) provided for %s; ' | ||
. 'all values must either be identifiers OR screen names. ' | ||
. 'You cannot provide items of both types.', | ||
$context | ||
)); | ||
} | ||
|
||
if ($detectedType === 'user_id') { | ||
$params['user_id'] = implode(',', $ids); | ||
return $params; | ||
} | ||
|
||
array_walk($ids, Closure::fromCallable([$this, 'validateScreenName'])); | ||
$params['screen_name'] = implode(',', $ids); | ||
return $params; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
[ | ||
{ | ||
"name": "Taylor Singletary", | ||
"screen_name": "episod", | ||
"id": 819797, | ||
"id_str": "819797", | ||
"connections": [ | ||
"following", | ||
"followed_by" | ||
] | ||
}, | ||
{ | ||
"name": "Twitter API", | ||
"screen_name": "twitterapi", | ||
"id": 6253282, | ||
"id_str": "6253282", | ||
"connections": [ | ||
"following", | ||
"followed_by" | ||
] | ||
}, | ||
{ | ||
"name": "White Leaf", | ||
"screen_name": "whiteleaf", | ||
"id": 22479443, | ||
"id_str": "22479443", | ||
"connections": [ | ||
"followed_by", | ||
"muting" | ||
] | ||
}, | ||
{ | ||
"name": "Andy Piper", | ||
"screen_name": "andypiper", | ||
"id": 786491, | ||
"id_str": "786491", | ||
"connections": [ | ||
"none" | ||
] | ||
} | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be split into a singular and a plural method (new API).
Widening the accepted types is also a BC break, as it breaks LSP compliance for subclasses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it considered widening if no typehint is present, though?
Further, the method is a 1:1 map with the Twitter API, which has always allowed these values; this is a fix to ensure that we allow what the API allows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because the previous API was about a single identifier. The problem is child classes, not really the twitter API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing so would break the 1:1 relationship with the Twitter API, though. Additionally, the changes still support usage of singular identifiers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the point about parameter widening and child classes. However, an API wrapper like this is less likely to have somebody extend an existing method that maps to the API, and more likely to extend to add methods to interact with API endpoints that have not yet been implemented (e.g., new methods). The primary factor in this case is that the consumer API does not break, versus the extension API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Breaking 1:1 naming is not a problem, but introducing mixed endpoints is.
The consumer doesn't break anyway: if they need to use multi-value APIs, they just use the specialised method.
This is a limitation of PHP itself, since method overloading isn't allowed.
Regardless of BC, adding an union type is a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, as a consumer of this component, having the end points match the twitter API is better. The API does not differentiate between calling the loopkups with a single ID or multiple IDs. Not sure why the component should.
This would be a breaking BC as I actually have working code using these methods.