Skip to content
This repository was archived by the owner on Jan 21, 2020. It is now read-only.

Commit 7a02a11

Browse files
committed
Merge branch 'hotfix/lookup-with-multiple-users'
Close #45
2 parents 1ec35fd + 25d9816 commit 7a02a11

File tree

5 files changed

+410
-16
lines changed

5 files changed

+410
-16
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
All notable changes to this project will be documented in this file, in reverse chronological order by release.
44

5-
## 3.0.1 - TBD
5+
## 3.0.1 - 2017-08-17
66

77
### Added
88

@@ -24,6 +24,12 @@ All notable changes to this project will be documented in this file, in reverse
2424
- `owner_id`: a valid user identifier (integer)
2525
- `owner_screen_name`: a valid user screen name (string)
2626

27+
- [#45](https://github.com/zendframework/ZendService_Twitter/pull/45) adds
28+
the ability to pass arrays of user identifiers OR screen names to each of the
29+
`usersLookup()` and `friendshipsLookup()` methods, giving them parity with the
30+
Twitter API. In each case, if you pass an array of values, the MUST be all
31+
user identifiers OR user screen names; you cannot mix the types.
32+
2733
### Changed
2834

2935
- Nothing.

src/Twitter.php

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace ZendService\Twitter;
99

10+
use Closure;
1011
use Traversable;
1112
use ZendOAuth as OAuth;
1213
use Zend\Http;
@@ -803,14 +804,21 @@ public function friendshipsCreate($id, array $params = []) : Response
803804
*
804805
* Returns the next cursor if there are more to be returned.
805806
*
806-
* @param int|string $id
807+
* $id may be one of any of the following:
808+
*
809+
* - a single user identifier
810+
* - a single screen name
811+
* - a list of user identifiers
812+
* - a list of screen names
813+
*
814+
* @param int|string|array $id
807815
* @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out
808816
* @throws Exception\DomainException if unable to decode JSON payload
809817
*/
810818
public function friendshipsLookup($id, array $params = []) : Response
811819
{
812820
$path = 'friendships/lookup';
813-
$params = $this->createUserParameter($id, $params);
821+
$params = $this->createUserListParameter($id, $params, __METHOD__);
814822
return $this->get($path, $params);
815823
}
816824

@@ -1335,15 +1343,21 @@ public function statusesUserTimeline(array $options = []) : Response
13351343
*
13361344
* This is the most effecient way of gathering bulk user data.
13371345
*
1338-
* @param int|string $id
1346+
* $id may be one of any of the following:
1347+
*
1348+
* - a single user identifier
1349+
* - a single screen name
1350+
* - a list of user identifiers
1351+
* - a list of screen names
1352+
*
1353+
* @param int|string|array $id
13391354
* @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out
13401355
* @throws Exception\DomainException if unable to decode JSON payload
13411356
*/
13421357
public function usersLookup($id, array $params = []) : Response
13431358
{
13441359
$path = 'users/lookup';
1345-
// $params = $this->createUserParameter($id, $params);
1346-
$params['user_id'] = $id;
1360+
$params = $this->createUserListParameter($id, $params, __METHOD__);
13471361
return $this->post($path, $params);
13481362
}
13491363

@@ -1446,16 +1460,17 @@ public function init(string $path, Http\Client $client) : void
14461460
* returned verbatim. Otherwise, a zero is returned.
14471461
*
14481462
* @param string|int $int
1449-
* @return string|int
14501463
*/
1451-
protected function validInteger($int)
1464+
protected function validInteger($int) : int
14521465
{
1453-
if (is_int($int)) {
1466+
if (is_int($int) && $int > -1) {
14541467
return $int;
14551468
}
1469+
14561470
if (is_string($int) && preg_match('/^(\d+)$/', $int)) {
14571471
return $int;
14581472
}
1473+
14591474
return 0;
14601475
}
14611476

@@ -1467,12 +1482,11 @@ protected function validInteger($int)
14671482
protected function validateScreenName(string $name) : string
14681483
{
14691484
if (! is_string($name) || ! preg_match('/^[a-zA-Z0-9_]{1,15}$/', $name)) {
1470-
throw new Exception\InvalidArgumentException(
1471-
'Screen name, "'
1472-
. $name
1473-
. '" should only contain alphanumeric characters and'
1474-
. ' underscores, and not exceed 15 characters.'
1475-
);
1485+
throw new Exception\InvalidArgumentException(sprintf(
1486+
'Screen name, "%s" should only contain alphanumeric characters and'
1487+
. ' underscores, and not exceed 15 characters.',
1488+
$name
1489+
));
14761490
}
14771491
return $name;
14781492
}
@@ -1513,15 +1527,106 @@ protected function performPost(string $method, $data, Http\Client $client) : Htt
15131527
*
15141528
* @param int|string $id
15151529
* @param array $params
1530+
* @throws Exception\InvalidArgumentException if the value is neither an integer nor a string
15161531
*/
15171532
protected function createUserParameter($id, array $params) : array
15181533
{
1519-
if ($this->validInteger($id)) {
1534+
if (! is_string($id) && ! is_int($id)) {
1535+
throw new Exception\InvalidArgumentException(sprintf(
1536+
'$id must be an integer or a string, received %s',
1537+
is_object($id) ? get_class($id) : gettype($id)
1538+
));
1539+
}
1540+
1541+
if (0 !== $this->validInteger($id)) {
15201542
$params['user_id'] = $id;
15211543
return $params;
15221544
}
15231545

1546+
if (! is_string($id)) {
1547+
throw new Exception\InvalidArgumentException(sprintf(
1548+
'$id must be an integer or a string, received %s',
1549+
gettype($id)
1550+
));
1551+
}
1552+
15241553
$params['screen_name'] = $this->validateScreenName($id);
15251554
return $params;
15261555
}
1556+
1557+
/**
1558+
* Prepares a list of identifiers for use with endpoints accepting lists of users.
1559+
*
1560+
* The various `lookup` endpoints allow passing either:
1561+
*
1562+
* - a single user identifier
1563+
* - a single screen name
1564+
* - a list of user identifiers
1565+
* - a list of screen names
1566+
*
1567+
* This method checks for each of these conditions. For scalar $ids, it
1568+
* proxies to {@link createUserParameter}. Otherwise, it checks to ensure
1569+
* that all values are of the same type. For identifiers, it then
1570+
* concatenates the values and returns them in the `user_id` parameter.
1571+
* For screen names, it validates them first, before concatenating and
1572+
* returning them via the `screen_name` parameter.
1573+
*
1574+
* @param int|string|array $ids
1575+
* @throws Exception\InvalidArgumentException for a non-int/string/array $ids value.
1576+
* @throws Exception\InvalidArgumentException if an array of $ids exceeds 100 items.
1577+
* @throws Exception\InvalidArgumentException if an array of $ids are not all of the same type.
1578+
* @throws Exception\InvalidArgumentException if any screen name element is invalid.
1579+
*/
1580+
protected function createUserListParameter($ids, array $params, string $context) : array
1581+
{
1582+
if (! is_array($ids)) {
1583+
return $this->createUserParameter($ids, $params);
1584+
}
1585+
1586+
if (100 < count($ids)) {
1587+
throw new Exception\InvalidArgumentException(sprintf(
1588+
'Lists of identifier(s) or screen name(s) provided for %s; '
1589+
. 'must contain no more than 100 items. '
1590+
. 'Received %d',
1591+
$context,
1592+
count($ids)
1593+
));
1594+
}
1595+
1596+
$detectedType = array_reduce($ids, function ($detectedType, $id) {
1597+
if (false === $detectedType) {
1598+
return $detectedType;
1599+
}
1600+
1601+
$idType = 0 !== $this->validInteger($id)
1602+
? 'user_id'
1603+
: 'screen_name';
1604+
1605+
if (null === $detectedType) {
1606+
return $idType;
1607+
}
1608+
1609+
return $detectedType === $idType
1610+
? $detectedType
1611+
: false;
1612+
}, null);
1613+
1614+
if (false === $detectedType) {
1615+
throw new Exception\InvalidArgumentException(sprintf(
1616+
'Invalid identifier(s) or screen name(s) provided for %s; '
1617+
. 'all values must either be identifiers OR screen names. '
1618+
. 'You cannot provide items of both types.',
1619+
$context
1620+
));
1621+
}
1622+
1623+
if ($detectedType === 'user_id') {
1624+
$params['user_id'] = implode(',', $ids);
1625+
return $params;
1626+
}
1627+
1628+
array_walk($ids, Closure::fromCallable([$this, 'validateScreenName']));
1629+
$params['screen_name'] = implode(',', $ids);
1630+
return $params;
1631+
}
15271632
}

test/TwitterTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,4 +1130,99 @@ public function testListsMembersRaisesExceptionIfSlugPassedWithInvalidOwnerScree
11301130
$this->expectExceptionMessage('invalid owner_screen_name');
11311131
$twitter->lists->members('zendframework', ['owner_screen_name' => $owner]);
11321132
}
1133+
1134+
public function userIdentifierProvider() : iterable
1135+
{
1136+
yield 'single-user_id' => [111, 'user_id'];
1137+
yield 'single-screen_name' => ['zfdevteam', 'screen_name'];
1138+
yield 'multi-user_id' => [range(100, 150), 'user_id'];
1139+
yield 'multi-screen_name' => [range('a', 'z'), 'screen_name'];
1140+
}
1141+
1142+
/**
1143+
* @dataProvider userIdentifierProvider
1144+
* @param mixed $id
1145+
*/
1146+
public function testUsersLookupAcceptsAllSupportedUserIdentifiers($id, string $paramKey)
1147+
{
1148+
$expected = is_array($id)
1149+
? $expected = implode(',', $id)
1150+
: $id;
1151+
1152+
$twitter = new Twitter\Twitter();
1153+
$twitter->setHttpClient($this->stubOAuthClient(
1154+
'users/lookup.json',
1155+
Http\Request::METHOD_POST,
1156+
'users.lookup.json',
1157+
[$paramKey => $expected]
1158+
));
1159+
1160+
$finalResponse = $twitter->users->lookup($id);
1161+
$this->assertInstanceOf(TwitterResponse::class, $finalResponse);
1162+
}
1163+
1164+
/**
1165+
* @dataProvider userIdentifierProvider
1166+
* @param mixed $id
1167+
*/
1168+
public function testFriendshipsLookupAcceptsAllSupportedUserIdentifiers($id, string $paramKey)
1169+
{
1170+
$expected = is_array($id)
1171+
? $expected = implode(',', $id)
1172+
: $id;
1173+
1174+
$twitter = new Twitter\Twitter();
1175+
$twitter->setHttpClient($this->stubOAuthClient(
1176+
'friendships/lookup.json',
1177+
Http\Request::METHOD_GET,
1178+
'friendships.lookup.json',
1179+
[$paramKey => $expected]
1180+
));
1181+
1182+
$finalResponse = $twitter->friendships->lookup($id);
1183+
$this->assertInstanceOf(TwitterResponse::class, $finalResponse);
1184+
}
1185+
1186+
public function invalidUserIdentifierProvider() : iterable
1187+
{
1188+
yield 'null' => [null, 'integer or a string'];
1189+
yield 'true' => [true, 'integer or a string'];
1190+
yield 'false' => [false, 'integer or a string'];
1191+
yield 'zero-float' => [0.0, 'integer or a string'];
1192+
yield 'float' => [1.1, 'integer or a string'];
1193+
yield 'empty-string' => ['', 'alphanumeric'];
1194+
yield 'malformed-string' => ['not a valid screen name', 'alphanumeric'];
1195+
yield 'array-too-many' => [range(1, 200), 'no more than 100'];
1196+
yield 'array-type-mismatch' => [[1, 'screenname'], 'identifiers OR screen names'];
1197+
yield 'array-invalid-screen-name' => [['not a valid screen name'], 'alphanumeric'];
1198+
yield 'object' => [new stdClass(), 'integer or a string'];
1199+
}
1200+
1201+
/**
1202+
* @dataProvider invalidUserIdentifierProvider
1203+
* @param mixed $ids
1204+
*/
1205+
public function testUsersLookupRaisesExceptionIfInvalidIdentifiersProvided($ids, string $expectedMessage)
1206+
{
1207+
$twitter = new Twitter\Twitter();
1208+
1209+
$this->expectException(Twitter\Exception\InvalidArgumentException::class);
1210+
$this->expectExceptionMessage($expectedMessage);
1211+
1212+
$twitter->users->lookup($ids);
1213+
}
1214+
1215+
/**
1216+
* @dataProvider invalidUserIdentifierProvider
1217+
* @param mixed $ids
1218+
*/
1219+
public function testFriendshipsLookupRaisesExceptionIfInvalidIdentifiersProvided($ids, string $expectedMessage)
1220+
{
1221+
$twitter = new Twitter\Twitter();
1222+
1223+
$this->expectException(Twitter\Exception\InvalidArgumentException::class);
1224+
$this->expectExceptionMessage($expectedMessage);
1225+
1226+
$twitter->friendships->lookup($ids);
1227+
}
11331228
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[
2+
{
3+
"name": "Taylor Singletary",
4+
"screen_name": "episod",
5+
"id": 819797,
6+
"id_str": "819797",
7+
"connections": [
8+
"following",
9+
"followed_by"
10+
]
11+
},
12+
{
13+
"name": "Twitter API",
14+
"screen_name": "twitterapi",
15+
"id": 6253282,
16+
"id_str": "6253282",
17+
"connections": [
18+
"following",
19+
"followed_by"
20+
]
21+
},
22+
{
23+
"name": "White Leaf",
24+
"screen_name": "whiteleaf",
25+
"id": 22479443,
26+
"id_str": "22479443",
27+
"connections": [
28+
"followed_by",
29+
"muting"
30+
]
31+
},
32+
{
33+
"name": "Andy Piper",
34+
"screen_name": "andypiper",
35+
"id": 786491,
36+
"id_str": "786491",
37+
"connections": [
38+
"none"
39+
]
40+
}
41+
]

0 commit comments

Comments
 (0)