wlogic/qbjs-parser
A lightweight Laravel/PHP parser for QBJS (QuickBooks JSON) data. It converts accounting records into structured PHP arrays and objects, making validation, querying, and integration simple. Ideal for importing customers, invoices, and transactions.
Install the package and inspect the source before wiring it into anything:
composer require wlogic/qbjs-parser
Where to look first:
src/ — the parser class and any operator-mapping/config file. The public entry point is small; read it end to end before trusting the README examples.tests/ (if present) — the best specification for operator names, nesting behaviour, and the shape of the returned object (SQL string + bindings, or a query object).First use case — a filtered index endpoint that receives the jQuery QueryBuilder JSON and returns paginated rows:
Route::post('/orders/filter', function (Request $request) {
$parser = new \WLogic\QBJSParser\Parser(); // confirm exact class in src/
$result = $parser->parse($request->input('rules'));
return DB::table('orders')
->whereRaw($result->getSql(), $result->getBindings())
->paginate(25);
});
The payload coming from the front end looks like:
{
"condition": "AND",
"rules": [
{ "field": "status", "operator": "equal", "value": "paid" },
{ "field": "total", "operator": "greater_or_equal", "value": 100 },
{
"condition": "OR",
"rules": [
{ "field": "country", "operator": "in", "value": ["DE", "FR"] },
{ "field": "vip", "operator": "equal", "value": true }
]
}
]
}
Start with a flat, single-level AND group, dump the generated SQL and bindings, and only then add nesting. That sequence catches operator-name mismatches in minutes instead of debugging a nested query.
Wrap the parser in a single service. Don't call it from controllers directly. A RuleQueryService centralises the field whitelist, operator whitelist, and output composition, and gives you one place to swap parsers if the package stalls:
class RuleQueryService
{
public function apply(Builder $query, array $rules): Builder
{
$result = (new Parser())->parse($rules);
return $query->whereRaw($result->getSql(), $result->getBindings());
}
}
Field whitelisting / aliasing. jQuery QueryBuilder sends whatever id you gave the field on the front end. That is not necessarily a column name. Keep an explicit map (['customer_name' => 'customers.name']) and reject anything not in it before parsing — this is both a security control and a way to support joined tables.
Compose with the base query. The parser has no idea about your tenancy, soft-deletes, or default scopes. Always apply the parser after those constraints so the user can only narrow an already-restricted result set:
$query = Order::where('tenant_id', $tenant->id)->whereNull('deleted_at');
$query = $service->apply($query, $request->input('rules', []));
Cache parsed output per request payload. The JSON rule tree is deterministic; hashing it and storing the SQL + bindings for a few minutes is a cheap win if the endpoint is hot, since parsing happens on every keystroke-triggered refresh from the UI.
Testing pattern. Keep a set of fixtures — one flat AND, one nested OR, one with in and between, one with an empty rule set — and assert against getSql() and getBindings() rather than running the query. Assert SQL structure loosely (normalise whitespace) so minor formatting changes don't break tests.
Front-end contract. Return the parsed rule tree back to the client so the UI can redisplay the active filters, and version the JSON shape in an Accept header or a ?v= query param if you expect the jQuery QueryBuilder config to change.
Feature flag the endpoint. With zero dependents and a single 2024 release, rollout behind a flag lets you switch back to hand-rolled where clauses if a specific operator misbehaves.
field. The rule's field name almost certainly flows into the SQL string. If the parser doesn't validate it, you must. Whitelist to known columns and never pass the raw value through.equal, not_equal, less, greater, less_or_equal, greater_or_equal, begins_with, contains, in, not_in, between, is_null, is_not_null. Confirm each is supported in the parser's operator map before advertising it in the UI, and hide unsupported operators client-side."value": "100" and "value": 100 behave differently across engines. Normalise types server-side based on the field's declared type (string/number/date/boolean) before handing the tree to the parser, especially for between and in.OR groups plus whereRaw can produce bindings in an order that doesn't match the SQL if you split the tree. Parse the whole tree in one call and pass the returned bindings verbatim.rules: [] with condition: "AND". Decide whether that means "no filter" or "return nothing" — most APIs want the former, so short-circuit on empty before calling the parser.is_null. The value key may be absent or null. Guard against Undefined index warnings, and remember whereRaw with a null binding produces LIMIT-like surprises on some drivers.IN empty array. {"operator": "in", "value": []} is valid on the front end but renders IN (), which is a syntax error on MySQL. Replace with a 1=0 short-circuit.? or named placeholders. Laravel's whereRaw expects ?; if the package emits :name, you'll need to bypass the builder and use DB::select.app/Support if you need to patch an operator, and pin the exact version in composer.json.dd($result->getSql(), $result->getBindings()) before execution is faster than reading stack traces — most failures are operator-name or column-name mismatches, not parser bugs.How can I help you explore Laravel packages today?