mirror of
https://github.com/halaxa/json-machine.git
synced 2025-03-15 17:09:39 +01:00
init
This commit is contained in:
commit
6b169b17b1
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea
|
||||
/vendor/
|
20
composer.json
Normal file
20
composer.json
Normal 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
1398
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
phpunit.xml
Normal file
7
phpunit.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<phpunit bootstrap="./test/bootstrap.php">
|
||||
<testsuites>
|
||||
<testsuite name="main">
|
||||
<directory>./test/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
8
src/Exception/InvalidArgumentException.php
Normal file
8
src/Exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace JsonIterator\Exception;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException
|
||||
{
|
||||
|
||||
}
|
8
src/Exception/PathNotFoundException.php
Normal file
8
src/Exception/PathNotFoundException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace JsonIterator\Exception;
|
||||
|
||||
class PathNotFoundException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
12
src/Exception/SyntaxError.php
Normal file
12
src/Exception/SyntaxError.php
Normal 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
63
src/JsonIterator.php
Normal 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
87
src/Lexer.php
Normal 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
217
src/Parser.php
Normal 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());
|
||||
}
|
||||
}
|
1
test/JsonStreamReaderTest/JsonIteratorTest.json
Normal file
1
test/JsonStreamReaderTest/JsonIteratorTest.json
Normal file
@ -0,0 +1 @@
|
||||
{"args": {"key":"value"}}
|
26
test/JsonStreamReaderTest/JsonIteratorTest.php
Normal file
26
test/JsonStreamReaderTest/JsonIteratorTest.php
Normal 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'],
|
||||
];
|
||||
}
|
||||
}
|
22
test/JsonStreamReaderTest/LexerTest.php
Normal file
22
test/JsonStreamReaderTest/LexerTest.php
Normal 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);
|
||||
}
|
||||
}
|
167
test/JsonStreamReaderTest/ParserTest.php
Normal file
167
test/JsonStreamReaderTest/ParserTest.php
Normal 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;
|
||||
}
|
||||
}
|
415
test/JsonStreamReaderTest/twitter_example_0.json
Normal file
415
test/JsonStreamReaderTest/twitter_example_0.json
Normal 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"
|
||||
}
|
||||
}
|
145
test/JsonStreamReaderTest/twitter_example_1.json
Normal file
145
test/JsonStreamReaderTest/twitter_example_1.json
Normal 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
4
test/bootstrap.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
Loading…
x
Reference in New Issue
Block a user