1
0
mirror of https://github.com/halaxa/json-machine.git synced 2025-03-15 17:09:39 +01:00
This commit is contained in:
Filip Halaxa 2015-10-06 18:02:03 +02:00 committed by Filip Halaxa
commit 6b169b17b1
18 changed files with 2602 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
/vendor/

BIN
composer Executable file

Binary file not shown.

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "halaxa/json-iterator",
"description": "Json parser consuming a stream of json data with easy, iterable api.",
"require": {
"php": ">=5.6",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"authors": [
{
"name": "Filip Halaxa",
"email": "filip@halaxa.cz"
}
],
"autoload" : {
"psr-4": {"JsonIterator\\": "src/"}
}
}

1398
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

7
phpunit.xml Normal file
View File

@ -0,0 +1,7 @@
<phpunit bootstrap="./test/bootstrap.php">
<testsuites>
<testsuite name="main">
<directory>./test/</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@ -0,0 +1,8 @@
<?php
namespace JsonIterator\Exception;
class InvalidArgumentException extends \InvalidArgumentException
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace JsonIterator\Exception;
class PathNotFoundException extends \RuntimeException
{
}

View File

@ -0,0 +1,12 @@
<?php
namespace JsonIterator\Exception;
class SyntaxError extends \RuntimeException
{
public function __construct($message = "", $position)
{
parent::__construct($message." At position $position.");
}
}

63
src/JsonIterator.php Normal file
View File

@ -0,0 +1,63 @@
<?php
namespace JsonIterator;
use IteratorAggregate;
use JsonIterator\Exception\InvalidArgumentException;
class JsonIterator implements IteratorAggregate
{
/**
* @var resource
*/
private $stream;
/**
* @var string
*/
private $jsonPointer;
public function __construct($stream, $jsonPointer = '')
{
if ( ! is_resource($stream) || get_resource_type($stream) !== 'stream') {
throw new InvalidArgumentException("Argument \$stream must be a valid stream resource.");
}
$this->stream = $stream;
$this->jsonPointer = $jsonPointer;
}
/**
* @param $string
* @param string $jsonPointer
* @return self
*/
public static function fromString($string, $jsonPointer = '')
{
return new static(fopen("data://text/plain,$string", 'r'), $jsonPointer);
}
/**
* @param $string
* @param string $jsonPointer
* @return self
*/
public static function fromFile($file, $jsonPointer = '')
{
return new static(fopen($file, 'r'), $jsonPointer);
}
/**
* @param $string
* @param string $jsonPointer
* @return self
*/
public static function fromStream($stream, $jsonPointer = '')
{
return new static($stream, $jsonPointer);
}
public function getIterator()
{
return new Parser(new Lexer($this->stream), $this->jsonPointer);
}
}

87
src/Lexer.php Normal file
View File

@ -0,0 +1,87 @@
<?php
namespace JsonIterator;
class Lexer implements \IteratorAggregate
{
/** @var resource */
private $stream;
private $position = 0;
/**
* @param resource $stream
*/
public function __construct($stream)
{
if ( ! is_resource($stream)) {
throw new Exception\InvalidArgumentException('Parameter $stream must be valid resource.');
}
$this->stream = $stream;
}
/**
* @return \Generator
*/
public function getIterator()
{
$inString = false;
$tokenBuffer = '';
$previousByte = null;
${' '} = 0;
${"\n"} = 0;
${"\r"} = 0;
${"\t"} = 0;
${'{'} = 1;
${'}'} = 1;
${'['} = 1;
${']'} = 1;
${':'} = 1;
${','} = 1;
while ('' !== ($bytes = fread($this->stream, 1024 * 8))) {
$bytesLength = strlen($bytes);
for ($i = 0; $i < $bytesLength; ++$i) {
$byte = $bytes[$i];
++$this->position;
if ($inString) {
if ($byte === '"' && $previousByte !== '\\') {
$inString = false;
} else {
$previousByte = $byte;
}
$tokenBuffer .= $byte;
continue;
}
if (isset($$byte)) {
if ($tokenBuffer !== '') {
yield $tokenBuffer;
$tokenBuffer = '';
}
if ($$byte) { // is not whitespace
yield $byte;
}
} else {
if ($byte === '"') {
$inString = true;
}
$tokenBuffer .= $byte;
}
}
}
if ($tokenBuffer !== '') {
yield $tokenBuffer;
}
}
/**
* @return int
*/
public function getPosition()
{
return $this->position;
}
}

217
src/Parser.php Normal file
View File

@ -0,0 +1,217 @@
<?php
namespace JsonIterator;
use JsonIterator\Exception\InvalidArgumentException;
use JsonIterator\Exception\PathNotFoundException;
use JsonIterator\Exception\SyntaxError;
class Parser implements \IteratorAggregate
{
const SCALAR_CONST = 1;
const SCALAR_STRING = 2;
const OBJECT_START = 4;
const OBJECT_END = 8;
const ARRAY_START = 16;
const ARRAY_END = 32;
const COMMA = 64;
const COLON = 128;
const AFTER_ARRAY_START = self::ANY_VALUE | self::ARRAY_END;
const AFTER_OBJECT_START = self::SCALAR_STRING | self::OBJECT_END;
const AFTER_ARRAY_VALUE = self::COMMA | self::ARRAY_END;
const AFTER_OBJECT_VALUE = self::COMMA | self::OBJECT_END;
const ANY_VALUE = self::OBJECT_START | self::ARRAY_START | self::SCALAR_CONST | self::SCALAR_STRING;
private $type = [
'n' => self::SCALAR_CONST,
'N' => self::SCALAR_CONST,
't' => self::SCALAR_CONST,
'T' => self::SCALAR_CONST,
'f' => self::SCALAR_CONST,
'F' => self::SCALAR_CONST,
'+' => self::SCALAR_CONST,
'-' => self::SCALAR_CONST,
'0' => self::SCALAR_CONST,
'1' => self::SCALAR_CONST,
'2' => self::SCALAR_CONST,
'3' => self::SCALAR_CONST,
'4' => self::SCALAR_CONST,
'5' => self::SCALAR_CONST,
'6' => self::SCALAR_CONST,
'7' => self::SCALAR_CONST,
'8' => self::SCALAR_CONST,
'9' => self::SCALAR_CONST,
'"' => self::SCALAR_STRING,
'{' => self::OBJECT_START,
'}' => self::OBJECT_END,
'[' => self::ARRAY_START,
']' => self::ARRAY_END,
',' => self::COMMA,
':' => self::COLON,
];
/** @var Lexer */
private $lexer;
/** @var string */
private $token;
/** @var string */
private $jsonPointerPath;
/** @var string */
private $jsonPointer;
/**
* @param \Traversable $lexer
* @param string $jsonPointer Follows json pointer RFC https://tools.ietf.org/html/rfc6901
*/
public function __construct(\Traversable $lexer, $jsonPointer = '')
{
if (0 === preg_match('_^(/(([^/~])|(~[01]))*)*$_', $jsonPointer, $matches)) {
throw new InvalidArgumentException("Given value '$jsonPointer' of \$jsonPointer is not valid JSON Pointer");
}
$this->lexer = $lexer;
$this->jsonPointer = $jsonPointer;
$this->jsonPointerPath = array_slice(array_map(function ($jsonPointerPart){
$jsonPointerPart = str_replace('~0', '~', str_replace('~1', '/', $jsonPointerPart));
return is_numeric($jsonPointerPart) ? (int) $jsonPointerPart : $jsonPointerPart;
}, explode('/', $jsonPointer)), 1);
}
/**
* @param \Generator $lexer
* @return \Generator
*/
public function getIterator()
{
// todo Allow to call getIterator only once per instance
$iteratorLevel = count($this->jsonPointerPath);
$iteratorStruct = null;
$currentPath = [];
$pathFound = false;
$currentLevel = -1;
$stack = [$currentLevel => null];
$jsonBuffer = '';
$key = null;
$previousToken = null;
$inArray = false;
$inObject = false;
$expectedType = self::OBJECT_START | self::ARRAY_START;
foreach ($this->lexer as $token) {
$this->token = $token;
$firstChar = $token[0];
if ( ! isset($this->type[$firstChar]) || ! ($this->type[$firstChar] & $expectedType)) {
$this->error("Unexpected symbol");
}
if ($currentLevel > $iteratorLevel || ($currentLevel === $iteratorLevel && $expectedType & self::ANY_VALUE)) {
$jsonBuffer .= $token;
}
if ($currentLevel < $iteratorLevel && $inArray && $expectedType & self::ANY_VALUE) {
$currentPath[$currentLevel] = isset($currentPath[$currentLevel]) ? (1+$currentPath[$currentLevel]) : 0;
}
switch ($firstChar) {
case '"':
if ($inObject && ($previousToken === ',' || $previousToken === '{')) {
$expectedType = self::COLON;
$previousToken = null;
if ($currentLevel === $iteratorLevel) {
$key = $token;
} elseif ($currentLevel < $iteratorLevel) {
$currentPath[$currentLevel] = json_decode($token);
}
break;
} else {
goto expectedTypeAfterValue;
}
case ',':
if ($inObject) {
$expectedType = self::SCALAR_STRING;
} else {
$expectedType = self::ANY_VALUE;
}
$previousToken = ',';
break;
case ':':
$expectedType = self::ANY_VALUE;
break;
case '{':
++$currentLevel;
if ($currentLevel === $iteratorLevel) {
$iteratorStruct = $token;
}
$stack[$currentLevel] = $token;
$inArray = !$inObject = true;
$expectedType = self::AFTER_OBJECT_START;
$previousToken = '{';
break;
case '[':
++$currentLevel;
if ($currentLevel === $iteratorLevel) {
$iteratorStruct = $token;
}
$stack[$currentLevel] = $token;
$inArray = !$inObject = false;
$expectedType = self::AFTER_ARRAY_START;
break;
case '}':
case ']':
--$currentLevel;
$inArray = !$inObject = $stack[$currentLevel] === '{';
default:
expectedTypeAfterValue:
if ($inArray) {
$expectedType = self::AFTER_ARRAY_VALUE;
} else {
$expectedType = self::AFTER_OBJECT_VALUE;
}
}
if ( ! $pathFound && $currentPath == $this->jsonPointerPath) {
$pathFound = true;
}
if ($currentLevel === $iteratorLevel && $jsonBuffer !== '') {
if ($currentPath == $this->jsonPointerPath) {
$value = json_decode($jsonBuffer, true);
if ($iteratorStruct === '[') {
yield $value;
} else {
yield json_decode($key) => $value;
}
}
$jsonBuffer = '';
}
}
if ($this->token === null) {
$this->error('Cannot iterate empty JSON');
}
if ( ! $pathFound) {
throw new PathNotFoundException("Path '{$this->jsonPointer}' was not found in json stream.");
}
}
/**
* @return array
*/
public function getJsonPointerPath()
{
return $this->jsonPointerPath;
}
/**
* @return string
*/
public function getJsonPointer()
{
return $this->jsonPointer;
}
private function error($msg)
{
throw new SyntaxError($msg." '".$this->token."'", $this->lexer->getPosition());
}
}

View File

@ -0,0 +1 @@
{"args": {"key":"value"}}

View File

@ -0,0 +1,26 @@
<?php
namespace JsonIteratorTest;
use JsonIterator\JsonIterator;
class JsonIteratorTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider dataFactories
*/
public function testFactories($methodName, ...$args)
{
$iterator = call_user_func_array(JsonIterator::class."::$methodName", $args);
$this->assertSame(["key" => "value"], iterator_to_array($iterator));
}
public function dataFactories()
{
return [
['fromStream', fopen('data://text/plain,{"args": {"key":"value"}}', 'r'), '/args'],
['fromString', '{"args": {"key":"value"}}', '/args'],
['fromFile', __DIR__ . '/JsonIteratorTest.json', '/args'],
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace JsonIteratorTest;
use JsonIterator\Lexer;
use JsonIterator\Exception;
class LexerTest extends \PHPUnit_Framework_TestCase
{
public function testGeneratesTokens()
{
$data = 'data://text/plain,{}[],:null,"string" false:true,1,100000,1.555{-56]"","\\""';
$expected = ['{','}','[',']',',',':','null',',','"string"','false',':','true',',','1',',','100000',',','1.555','{','-56',']','""',',','"\\""'];
$this->assertEquals($expected, iterator_to_array(new Lexer(fopen($data, 'r'))));
}
public function testThrowsIfNoResource()
{
$this->setExpectedException(Exception\InvalidArgumentException::class);
new Lexer(false);
}
}

View File

@ -0,0 +1,167 @@
<?php
namespace JsonIteratorTest;
use JsonIterator\Exception\InvalidArgumentException;
use JsonIterator\Exception\PathNotFoundException;
use JsonIterator\Exception\SyntaxError;
use JsonIterator\Lexer;
use JsonIterator\Parser;
class ParserTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider dataSyntax
*/
public function testSyntax($pathSpec, $json, $expectedResult)
{
$result = iterator_to_array($this->createParser($json, $pathSpec));
$this->assertEquals($expectedResult, $result);
}
public function dataSyntax()
{
return [
['', '{}', []],
['', '{"a": "b"}', ['a'=>'b']],
['', '{"a":{"b":{"c":1}}}', ['a'=>['b'=>['c'=>1]]]],
['', '[]', []],
['', '[null,true,false,"a",0,1,42.5]', [null,true,false,"a",0,1,42.5]],
['', '[{"c":1}]', [['c'=>1]]],
['', '[{"c":1},"string",{"d":2},false]', [['c'=>1],"string",['d'=>2],false]],
['', '[false,{"c":1},"string",{"d":2}]', [false,['c'=>1],"string",['d'=>2]]],
['', '[{"c":1,"d":2}]', [['c'=>1,'d'=>2]]],
['/', '{"":{"c":1,"d":2}}', ['c'=>1,'d'=>2]],
['/~0', '{"~":{"c":1,"d":2}}', ['c'=>1,'d'=>2]],
['/~1', '{"/":{"c":1,"d":2}}', ['c'=>1,'d'=>2]],
['/path', '{"path":{"c":1,"d":2}}', ['c'=>1,'d'=>2]],
['/path', '{"no":[null], "path":{"c":1,"d":2}}', ['c'=>1,'d'=>2]],
['/0', '[{"c":1,"d":2}, [null]]', ['c'=>1,'d'=>2]],
['/0/path', '[{"path":{"c":1,"d":2}}]', ['c'=>1,'d'=>2]],
['/1/path', '[[null], {"path":{"c":1,"d":2}}]', ['c'=>1,'d'=>2]],
['/path/0', '{"path":[{"c":1,"d":2}, [null]]}', ['c'=>1,'d'=>2]],
['/path/1', '{"path":[null,{"c":1,"d":2}, [null]]}', ['c'=>1,'d'=>2]],
['/path/to', '{"path":{"to":{"c":1,"d":2}}}', ['c'=>1,'d'=>2]],
['/0/0', '[{"0":{"c":1,"d":2}}]', ['c'=>1,'d'=>2]],
['/1/1', '[0,{"1":{"c":1,"d":2}}]', ['c'=>1,'d'=>2]],
];
}
public function testThrowsOnNotFoundPathSpec()
{
$parser = $this->createParser('{}', '/not/found');
$this->expectException(PathNotFoundException::class);
$this->expectExceptionMessage("Path '/not/found' was not found in json stream.");
iterator_to_array($parser);
}
/**
* @dataProvider dataGetPathSpec
*/
public function testGetPathSpec($pathSpec, array $expectedPathSpec)
{
$parser = $this->createParser('{}', $pathSpec);
$this->assertEquals($expectedPathSpec, $parser->getJsonPointerPath());
}
public function dataGetPathSpec()
{
return [
['/', ['']],
['////', ['', '', '', '']],
['/apple', ['apple']],
['/apple/pie', ['apple', 'pie']],
['/0/1 ', [0, '1 ']],
];
}
/**
* @dataProvider dataThrowsOnMalformedJsonPointer
*/
public function testThrowsOnMalformedJsonPointer($jsonPointer)
{
$this->expectException(InvalidArgumentException::class);
new Parser(new \ArrayObject(), $jsonPointer);
}
public function dataThrowsOnMalformedJsonPointer()
{
return [
['apple'],
['/apple/~'],
['apple/pie'],
['apple/pie/'],
[' /apple/pie/'],
];
}
/**
* @dataProvider dataSyntaxError
*/
public function testSyntaxError($notIterableJson, $exception = SyntaxError::class)
{
$this->expectException($exception);
iterator_to_array($this->createParser($notIterableJson));
}
public function dataSyntaxError()
{
return [
['[}'],
['{]'],
['null'],
['true'],
['false'],
['0'],
['100'],
['"string"'],
['}'],
[']'],
[','],
[':'],
[''],
['[null null]'],
['["string" "string"]'],
['[,"string","string"]'],
['["string",,"string"]'],
['["string","string",]'],
];
}
private function createParser($json, $jsonPointer = '')
{
return new Parser(new Lexer(fopen("data://text/plain,$json", 'r')), $jsonPointer);
}
public function testPerformance()
{
$tmpJsonFileName = $this->createBigJsonFile();
$tmpJson = fopen($tmpJsonFileName, 'r');
$parser = new Parser(new Lexer($tmpJson));
$start = microtime(true);
foreach ($parser as $item) {
}
$time = microtime(true) - $start;
$filesizeMb = (filesize($tmpJsonFileName)/1024/1024);
var_dump(round($filesizeMb/$time, 2));
@unlink($tmpJsonFileName);
}
private function createBigJsonFile()
{
$tmpJson = tempnam(sys_get_temp_dir(), 'json_');
$f = fopen($tmpJson, 'w');
$separator = '';
fputs($f, '[');
for ($i=0; $i<1000; $i++) {
fputs($f, $separator);
fputs($f, file_get_contents(__DIR__.'/twitter_example_'. ($i%2) .'.json'));
$separator = ",\n\n";
}
fputs($f, ']');
fclose($f);
return $tmpJson;
}
}

View File

@ -0,0 +1,415 @@
{
"statuses": [
{
"coordinates": null,
"favorited": false,
"truncated": false,
"created_at": "Mon Sep 24 03:35:21 +0000 2012",
"id_str": "250075927172759552",
"entities": {
"urls": [
],
"hashtags": [
{
"text": "freebandnames",
"indices": [
20,
34
]
}
],
"user_mentions": [
]
},
"in_reply_to_user_id_str": null,
"contributors": null,
"text": "Aggressive Ponytail #freebandnames",
"metadata": {
"iso_language_code": "en",
"result_type": "recent"
},
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"id": 250075927172759552,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"user": {
"profile_sidebar_fill_color": "DDEEF6",
"profile_sidebar_border_color": "C0DEED",
"profile_background_tile": false,
"name": "Sean Cummings",
"profile_image_url": "http://a0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg",
"created_at": "Mon Apr 26 06:01:55 +0000 2010",
"location": "LA, CA",
"follow_request_sent": null,
"profile_link_color": "0084B4",
"is_translator": false,
"id_str": "137238150",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "",
"indices": [
0,
0
]
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": true,
"contributors_enabled": false,
"favourites_count": 0,
"url": null,
"profile_image_url_https": "https://si0.twimg.com/profile_images/2359746665/1v6zfgqo8g0d3mk7ii5s_normal.jpeg",
"utc_offset": -28800,
"id": 137238150,
"profile_use_background_image": true,
"listed_count": 2,
"profile_text_color": "333333",
"lang": "en",
"followers_count": 70,
"protected": false,
"notifications": null,
"profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme1/bg.png",
"profile_background_color": "C0DEED",
"verified": false,
"geo_enabled": true,
"time_zone": "Pacific Time (US & Canada)",
"description": "Born 330 Live 310",
"default_profile_image": false,
"profile_background_image_url": "http://a0.twimg.com/images/themes/theme1/bg.png",
"statuses_count": 579,
"friends_count": 110,
"following": null,
"show_all_inline_media": false,
"screen_name": "sean_cummings"
},
"in_reply_to_screen_name": null,
"source": "Twitter for Mac",
"in_reply_to_status_id": null
},
{
"coordinates": null,
"favorited": false,
"truncated": false,
"created_at": "Fri Sep 21 23:40:54 +0000 2012",
"id_str": "249292149810667520",
"entities": {
"urls": [
],
"hashtags": [
{
"text": "FreeBandNames",
"indices": [
20,
34
]
}
],
"user_mentions": [
]
},
"in_reply_to_user_id_str": null,
"contributors": null,
"text": "Thee Namaste Nerdz. #FreeBandNames",
"metadata": {
"iso_language_code": "pl",
"result_type": "recent"
},
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"id": 249292149810667520,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"user": {
"profile_sidebar_fill_color": "DDFFCC",
"profile_sidebar_border_color": "BDDCAD",
"profile_background_tile": true,
"name": "Chaz Martenstein",
"profile_image_url": "http://a0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg",
"created_at": "Tue Apr 07 19:05:07 +0000 2009",
"location": "Durham, NC",
"follow_request_sent": null,
"profile_link_color": "0084B4",
"is_translator": false,
"id_str": "29516238",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "http://bullcityrecords.com/wnng/",
"indices": [
0,
32
]
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": false,
"contributors_enabled": false,
"favourites_count": 8,
"url": "http://bullcityrecords.com/wnng/",
"profile_image_url_https": "https://si0.twimg.com/profile_images/447958234/Lichtenstein_normal.jpg",
"utc_offset": -18000,
"id": 29516238,
"profile_use_background_image": true,
"listed_count": 118,
"profile_text_color": "333333",
"lang": "en",
"followers_count": 2052,
"protected": false,
"notifications": null,
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/9423277/background_tile.bmp",
"profile_background_color": "9AE4E8",
"verified": false,
"geo_enabled": false,
"time_zone": "Eastern Time (US & Canada)",
"description": "You will come to Durham, North Carolina. I will sell you some records then, here in Durham, North Carolina. Fun will happen.",
"default_profile_image": false,
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/9423277/background_tile.bmp",
"statuses_count": 7579,
"friends_count": 348,
"following": null,
"show_all_inline_media": true,
"screen_name": "bullcityrecords"
},
"in_reply_to_screen_name": null,
"source": "web",
"in_reply_to_status_id": null
},
{
"coordinates": null,
"favorited": false,
"truncated": false,
"created_at": "Fri Sep 21 23:30:20 +0000 2012",
"id_str": "249289491129438208",
"entities": {
"urls": [
],
"hashtags": [
{
"text": "freebandnames",
"indices": [
29,
43
]
}
],
"user_mentions": [
]
},
"in_reply_to_user_id_str": null,
"contributors": null,
"text": "Mexican Heaven, Mexican Hell #freebandnames",
"metadata": {
"iso_language_code": "en",
"result_type": "recent"
},
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"id": 249289491129438208,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"user": {
"profile_sidebar_fill_color": "99CC33",
"profile_sidebar_border_color": "829D5E",
"profile_background_tile": false,
"name": "Thomas John Wakeman",
"profile_image_url": "http://a0.twimg.com/profile_images/2219333930/Froggystyle_normal.png",
"created_at": "Tue Sep 01 21:21:35 +0000 2009",
"location": "Kingston New York",
"follow_request_sent": null,
"profile_link_color": "D02B55",
"is_translator": false,
"id_str": "70789458",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "",
"indices": [
0,
0
]
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": false,
"contributors_enabled": false,
"favourites_count": 19,
"url": null,
"profile_image_url_https": "https://si0.twimg.com/profile_images/2219333930/Froggystyle_normal.png",
"utc_offset": -18000,
"id": 70789458,
"profile_use_background_image": true,
"listed_count": 1,
"profile_text_color": "3E4415",
"lang": "en",
"followers_count": 63,
"protected": false,
"notifications": null,
"profile_background_image_url_https": "https://si0.twimg.com/images/themes/theme5/bg.gif",
"profile_background_color": "352726",
"verified": false,
"geo_enabled": false,
"time_zone": "Eastern Time (US & Canada)",
"description": "Science Fiction Writer, sort of. Likes Superheroes, Mole People, Alt. Timelines.",
"default_profile_image": false,
"profile_background_image_url": "http://a0.twimg.com/images/themes/theme5/bg.gif",
"statuses_count": 1048,
"friends_count": 63,
"following": null,
"show_all_inline_media": false,
"screen_name": "MonkiesFist"
},
"in_reply_to_screen_name": null,
"source": "web",
"in_reply_to_status_id": null
},
{
"coordinates": null,
"favorited": false,
"truncated": false,
"created_at": "Fri Sep 21 22:51:18 +0000 2012",
"id_str": "249279667666817024",
"entities": {
"urls": [
],
"hashtags": [
{
"text": "freebandnames",
"indices": [
20,
34
]
}
],
"user_mentions": [
]
},
"in_reply_to_user_id_str": null,
"contributors": null,
"text": "The Foolish Mortals #freebandnames",
"metadata": {
"iso_language_code": "en",
"result_type": "recent"
},
"retweet_count": 0,
"in_reply_to_status_id_str": null,
"id": 249279667666817024,
"geo": null,
"retweeted": false,
"in_reply_to_user_id": null,
"place": null,
"user": {
"profile_sidebar_fill_color": "BFAC83",
"profile_sidebar_border_color": "615A44",
"profile_background_tile": true,
"name": "Marty Elmer",
"profile_image_url": "http://a0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png",
"created_at": "Mon May 04 00:05:00 +0000 2009",
"location": "Wisconsin, USA",
"follow_request_sent": null,
"profile_link_color": "3B2A26",
"is_translator": false,
"id_str": "37539828",
"entities": {
"url": {
"urls": [
{
"expanded_url": null,
"url": "http://www.omnitarian.me",
"indices": [
0,
24
]
}
]
},
"description": {
"urls": [
]
}
},
"default_profile": false,
"contributors_enabled": false,
"favourites_count": 647,
"url": "http://www.omnitarian.me",
"profile_image_url_https": "https://si0.twimg.com/profile_images/1629790393/shrinker_2000_trans_normal.png",
"utc_offset": -21600,
"id": 37539828,
"profile_use_background_image": true,
"listed_count": 52,
"profile_text_color": "000000",
"lang": "en",
"followers_count": 608,
"protected": false,
"notifications": null,
"profile_background_image_url_https": "https://si0.twimg.com/profile_background_images/106455659/rect6056-9.png",
"profile_background_color": "EEE3C4",
"verified": false,
"geo_enabled": false,
"time_zone": "Central Time (US & Canada)",
"description": "Cartoonist, Illustrator, and T-Shirt connoisseur",
"default_profile_image": false,
"profile_background_image_url": "http://a0.twimg.com/profile_background_images/106455659/rect6056-9.png",
"statuses_count": 3575,
"friends_count": 249,
"following": null,
"show_all_inline_media": true,
"screen_name": "Omnitarian"
},
"in_reply_to_screen_name": null,
"source": "Twitter for iPhone",
"in_reply_to_status_id": null
}
],
"search_metadata": {
"max_id": 250126199840518145,
"since_id": 24012619984051000,
"refresh_url": "?since_id=250126199840518145&q=%23freebandnames&result_type=mixed&include_entities=1",
"next_results": "?max_id=249279667666817023&q=%23freebandnames&count=4&include_entities=1&result_type=mixed",
"count": 4,
"completed_in": 0.035,
"since_id_str": "24012619984051000",
"query": "%23freebandnames",
"max_id_str": "250126199840518145"
}
}

View File

@ -0,0 +1,145 @@
{
"text": "RT @PostGradProblem: In preparation for the NFL lockout, I will be spending twice as much time analyzing my fantasy baseball team during ...",
"truncated": true,
"in_reply_to_user_id": null,
"in_reply_to_status_id": null,
"favorited": false,
"source": "<a href=\"http://twitter.com/\" rel=\"nofollow\">Twitter for iPhone</a>",
"in_reply_to_screen_name": null,
"in_reply_to_status_id_str": null,
"id_str": "54691802283900928",
"entities": {
"user_mentions": [
{
"indices": [
3,
19
],
"screen_name": "PostGradProblem",
"id_str": "271572434",
"name": "PostGradProblems",
"id": 271572434
}
],
"urls": [ ],
"hashtags": [ ]
},
"contributors": null,
"retweeted": false,
"in_reply_to_user_id_str": null,
"place": null,
"retweet_count": 4,
"created_at": "Sun Apr 03 23:48:36 +0000 2011",
"retweeted_status": {
"text": "In preparation for the NFL lockout, I will be spending twice as much time analyzing my fantasy baseball team during company time. #PGP",
"truncated": false,
"in_reply_to_user_id": null,
"in_reply_to_status_id": null,
"favorited": false,
"source": "<a href=\"http://www.hootsuite.com\" rel=\"nofollow\">HootSuite</a>",
"in_reply_to_screen_name": null,
"in_reply_to_status_id_str": null,
"id_str": "54640519019642881",
"entities": {
"user_mentions": [ ],
"urls": [ ],
"hashtags": [
{
"text": "PGP",
"indices": [
130,
134
]
}
]
},
"contributors": null,
"retweeted": false,
"in_reply_to_user_id_str": null,
"place": null,
"retweet_count": 4,
"created_at": "Sun Apr 03 20:24:49 +0000 2011",
"user": {
"notifications": null,
"profile_use_background_image": true,
"statuses_count": 31,
"profile_background_color": "C0DEED",
"followers_count": 3066,
"profile_image_url": "http://a2.twimg.com/profile_images/1285770264/PGP_normal.jpg",
"listed_count": 6,
"profile_background_image_url": "http://a3.twimg.com/a/1301071706/images/themes/theme1/bg.png",
"description": "",
"screen_name": "PostGradProblem",
"default_profile": true,
"verified": false,
"time_zone": null,
"profile_text_color": "333333",
"is_translator": false,
"profile_sidebar_fill_color": "DDEEF6",
"location": "",
"id_str": "271572434",
"default_profile_image": false,
"profile_background_tile": false,
"lang": "en",
"friends_count": 21,
"protected": false,
"favourites_count": 0,
"created_at": "Thu Mar 24 19:45:44 +0000 2011",
"profile_link_color": "0084B4",
"name": "PostGradProblems",
"show_all_inline_media": false,
"follow_request_sent": null,
"geo_enabled": false,
"profile_sidebar_border_color": "C0DEED",
"url": null,
"id": 271572434,
"contributors_enabled": false,
"following": null,
"utc_offset": null
},
"id": 54640519019642880,
"coordinates": null,
"geo": null
},
"user": {
"notifications": null,
"profile_use_background_image": true,
"statuses_count": 351,
"profile_background_color": "C0DEED",
"followers_count": 48,
"profile_image_url": "http://a1.twimg.com/profile_images/455128973/gCsVUnofNqqyd6tdOGevROvko1_500_normal.jpg",
"listed_count": 0,
"profile_background_image_url": "http://a3.twimg.com/a/1300479984/images/themes/theme1/bg.png",
"description": "watcha doin in my waters?",
"screen_name": "OldGREG85",
"default_profile": true,
"verified": false,
"time_zone": "Hawaii",
"profile_text_color": "333333",
"is_translator": false,
"profile_sidebar_fill_color": "DDEEF6",
"location": "Texas",
"id_str": "80177619",
"default_profile_image": false,
"profile_background_tile": false,
"lang": "en",
"friends_count": 81,
"protected": false,
"favourites_count": 0,
"created_at": "Tue Oct 06 01:13:17 +0000 2009",
"profile_link_color": "0084B4",
"name": "GG",
"show_all_inline_media": false,
"follow_request_sent": null,
"geo_enabled": false,
"profile_sidebar_border_color": "C0DEED",
"url": null,
"id": 80177619,
"contributors_enabled": false,
"following": null,
"utc_offset": -36000
},
"id": 54691802283900930,
"coordinates": null,
"geo": null
}

4
test/bootstrap.php Normal file
View File

@ -0,0 +1,4 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';