phplrt 4.0

JSON

A JSON parser that produces plain PHP values - the result matches json_decode($input, true) exactly.

1$parser->parse(new Source('{"a": 1, "b": [true, null]}'))->value;
2// ['a' => 1, 'b' => [true, null]]

Every rule returns its value wrapped in a single class:

1namespace Json;
2
3final readonly class JsonValue
4{
5    public function __construct(
6        public mixed $value,
7    ) {}
8}

The wrapper is not decoration, it is what makes the grammar work. A reducer returning a plain array has its items spliced into the rule above, so an object would collapse into a flat list; and a reducer returning null means "no result", so null would come back as the raw token. An object means what it says in both cases.

Grammar

 1%skip  T_WHITESPACE  \s++
 2
 3%token T_STRING      "(?:[^"\\]|\\.)*+"
 4%token T_NUMBER      -?\d++(?:\.\d++)?(?:[eE][+-]?\d++)?
 5%token T_TRUE        true
 6%token T_FALSE       false
 7%token T_NULL        null
 8
 9%token T_BRACE_OPEN     \{
10%token T_BRACE_CLOSE    \}
11%token T_BRACKET_OPEN   \[
12%token T_BRACKET_CLOSE  \]
13%token T_COLON          :
14%token T_COMMA          ,
15
16%pragma root Value
17
18Value
19  : Object()
20  | Array()
21  | String()
22  | Number()
23  | True()
24  | False()
25  | Null()
26  ;
27
28// The members arrive as a flat list: key, value, key, value...
29Object -> {
30    $values = [];
31    $children = \is_array($children) ? $children : [];
32
33    for ($i = 0; isset($children[$i + 1]); $i += 2) {
34        $values[$children[$i]->value] = $children[$i + 1]->value;
35    }
36
37    return new \Json\JsonValue($values);
38}
39  : ::T_BRACE_OPEN::
40      (Member() (::T_COMMA:: Member())*)?
41    ::T_BRACE_CLOSE::
42  ;
43
44// No reducer: a member is merged into the object as a key/value pair
45Member : String() ::T_COLON:: Value() ;
46
47Array -> {
48    $values = [];
49
50    foreach (\is_array($children) ? $children : [] as $value) {
51        $values[] = $value->value;
52    }
53
54    return new \Json\JsonValue($values);
55}
56  : ::T_BRACKET_OPEN::
57      (Value() (::T_COMMA:: Value())*)?
58    ::T_BRACKET_CLOSE::
59  ;
60
61String -> { return new \Json\JsonValue(\json_decode($children->value)); }
62  : <T_STRING>
63  ;
64
65Number -> { return new \Json\JsonValue(0 + $children->value); }
66  : <T_NUMBER>
67  ;
68
69True  -> { return new \Json\JsonValue(true); }  : <T_TRUE> ;
70False -> { return new \Json\JsonValue(false); } : <T_FALSE> ;
71Null  -> { return new \Json\JsonValue(null); }  : <T_NULL> ;

Usage

1use Phplrt\Compiler\Compiler;
2use Phplrt\Source\File;
3use Phplrt\Source\Source;
4
5$parser = new Compiler()
6    ->load(new File(__DIR__ . '/grammar.pp3'))
7    ->getParser();
8
9$data = $parser->parse(new Source($json))->value;

Two details worth stealing for your own grammars. Member has no reducer, so its two values are merged into the object's list - which is exactly why the object reads them in pairs. And (Value() (::T_COMMA:: Value())*)? is the standard shape for a possibly-empty comma-separated list; writing (Value() ::T_COMMA::)* instead would demand a trailing comma.

25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.