JSON
A JSON parser that produces plain PHP values - the result matches
json_decode($input, true) exactly.
$parser->parse(new Source('{"a": 1, "b": [true, null]}'))->value;
// ['a' => 1, 'b' => [true, null]]
Every rule returns its value wrapped in a single class:
namespace Json;
final readonly class JsonValue
{
public function __construct(
public mixed $value,
) {}
}
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
%skip T_WHITESPACE \s++
%token T_STRING "(?:[^"\\]|\\.)*+"
%token T_NUMBER -?\d++(?:\.\d++)?(?:[eE][+-]?\d++)?
%token T_TRUE true
%token T_FALSE false
%token T_NULL null
%token T_BRACE_OPEN \{
%token T_BRACE_CLOSE \}
%token T_BRACKET_OPEN \[
%token T_BRACKET_CLOSE \]
%token T_COLON :
%token T_COMMA ,
%pragma root Value
Value
: Object()
| Array()
| String()
| Number()
| True()
| False()
| Null()
;
// The members arrive as a flat list: key, value, key, value...
Object -> {
$values = [];
$children = \is_array($children) ? $children : [];
for ($i = 0; isset($children[$i + 1]); $i += 2) {
$values[$children[$i]->value] = $children[$i + 1]->value;
}
return new \Json\JsonValue($values);
}
: ::T_BRACE_OPEN::
(Member() (::T_COMMA:: Member())*)?
::T_BRACE_CLOSE::
;
// No reducer: a member is merged into the object as a key/value pair
Member : String() ::T_COLON:: Value() ;
Array -> {
$values = [];
foreach (\is_array($children) ? $children : [] as $value) {
$values[] = $value->value;
}
return new \Json\JsonValue($values);
}
: ::T_BRACKET_OPEN::
(Value() (::T_COMMA:: Value())*)?
::T_BRACKET_CLOSE::
;
String -> { return new \Json\JsonValue(\json_decode($children->value)); }
: <T_STRING>
;
Number -> { return new \Json\JsonValue(0 + $children->value); }
: <T_NUMBER>
;
True -> { return new \Json\JsonValue(true); } : <T_TRUE> ;
False -> { return new \Json\JsonValue(false); } : <T_FALSE> ;
Null -> { return new \Json\JsonValue(null); } : <T_NULL> ;
Usage
use Phplrt\Compiler\Compiler;
use Phplrt\Source\File;
use Phplrt\Source\Source;
$parser = new Compiler()
->load(new File(__DIR__ . '/grammar.pp3'))
->getParser();
$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.