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.