nisalatp/dynamicreportgenerator

Metadata-centric dynamic reporting engine for Laravel with automatic BFS joins, virtual attributes, and attribute-level security (privacy-aware masking).

Maintainers

Package info

github.com/nisalatp/DynamicReportGenerator-Public

pkg:composer/nisalatp/dynamicreportgenerator

Transparency log

Statistics

Installs: 12

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.2.0 2026-06-27 10:50 UTC

This package is auto-updated.

Last update: 2026-07-27 11:51:00 UTC


README

Laravel PHP License Packagist Version Downloads

Hi there! πŸ‘‹ Welcome to the Dynamic Report Generator β€” a reporting engine that lets your non-technical users build their own complex, multi-table reports, safely, without you writing a single query for them.

If you're a Laravel developer, you've been here: you ship a beautiful dashboard, and immediately get hit with "can we add a column for the user's last order date? what about filtering by customers who spent over $500 but haven't logged in recently?" Before long you're writing endless LEFT JOINs and a new controller for every report request. It's a bottleneck. This package removes it.

πŸ“¦ Live on Packagist Β Β·Β  πŸ“š Hosted documentation Β Β·Β  βœ… Independently built & validated by a third-party developer using only the published package and its docs.

The visual report builder

The point-and-click report builder (from the bundled iStore demo) β€” pick a model and columns; the engine resolves the joins and compiles the SQL for you.

✨ Why You'll Love It

  • No more manual joins. A Graph-Theory Breadth-First Search reads your Eloquent relationships and finds the shortest join path between models automatically. You declare what you want; the engine works out how to join it.
  • Virtual Attributes (the secret sauce). Pre-register a heavy SQL expression β€” say, a user's lifetime value β€” as a named "Virtual Attribute". To your end-users it's just another column to tick.
  • Memory-efficient by design. The engine compiles your request and pushes the work down to the database; it does not hydrate thousands of Eloquent models into PHP just to aggregate them. Virtual Attributes run as correlated subqueries β€” O(1) application memory, whatever the row count.
  • Predictable. Call explainJoinPlan() to inspect the resolved JOIN path, or toRawSql() to see the exact compiled query β€” before you run it.
  • Secure by default. Built-in attribute-level security β€” mask or block any column per user or role β€” alongside report assignment and full audit logging.
  • 100% UI-agnostic. Vue, React, Livewire, AlpineJS, a Java client β€” even an LLM via the Model Context Protocol. They all speak the same JSON AST.

πŸ“¦ Installation

composer require nisalatp/dynamicreportgenerator

Requires PHP 8.2+ and Laravel 11, 12 or 13. The Service Provider and the DynamicReport facade are auto-discovered β€” no manual registration needed.

php artisan migrate    # creates the dynamic_* tables (saved reports, logs, virtual attributes)

Then list the models you want to expose for reporting in config/dynamicreportgenerator.php under reportable_models. See the docs for the full setup.

🧭 Architecture: a deterministic Build β†’ Save β†’ Run lifecycle

The engine deliberately separates what a report is (configuration) from running it (execution). A report definition is a strictly-typed Abstract Syntax Tree (AST) β€” never raw SQL β€” so the same definition always compiles to the same parameterised query. That separation is what lets you save, share and re-run reports predictably at scale.

High-level architecture

Phase What happens Key API
Build A frontend (or the fluent ReportBuilder) produces a typed AST: base model, join targets, columns, filters, group-bys, aggregates. No SQL. ReportBuilder, ReportRequest
Save The AST is persisted as a JSON payload (DB-agnostic β€” MySQL, PostgreSQL or SQLite), can be assigned to users, and every execution is audit-logged. saveReport(), assignReport(), getReportLogs()
Run The engine compiles the AST: BFS resolves the joins, Virtual Attributes are injected as subqueries, values are PDO-bound, and you get an Eloquent query back. generate(), generatePaginated(), loadAndGenerate(), exportToCsv()

πŸš€ Quick Start

1. Build and run a report

use App\Models\{User, Order};
use Nisalatp\DynamicReportGenerator\Builders\ReportBuilder;
use Nisalatp\DynamicReportGenerator\Facades\DynamicReport;

$request = ReportBuilder::forModel(User::class)
    ->withTarget(Order::class)                  // BFS finds & builds the User→Order join for you
    ->select(User::class, 'name')
    ->select(Order::class, 'status')
    ->filter(fn ($f) => $f->where(Order::class, 'status', 'completed'))
    ->build();

return DynamicReport::generate($request)->paginate(50);

No JOIN, no DB::raw, no per-report controller.

2. See exactly what it will do β€” before you run it

$plan = DynamicReport::explainJoinPlan($request);                    // the resolved JOIN path
$sql  = DynamicReport::toRawSql(DynamicReport::generate($request));  // the compiled SQL

3. Save a definition, then re-run it later

$saved = DynamicReport::saveReport('Completed orders by user', $request, userId: auth()->id());

// elsewhere β€” load the saved configuration and execute it (writes an audit-log entry)
return DynamicReport::loadAndGenerate($saved->id, executedByUserId: auth()->id())->paginate(50);

4. Virtual Attributes β€” expose heavy SQL as a simple column

Register a Virtual Attribute once (in a Service Provider or an admin screen); users then select it like any other column.

use Nisalatp\DynamicReportGenerator\Builders\VirtualAttributeBuilder;

VirtualAttributeBuilder::create('Lifetime Value')
    ->forBaseModel(User::class)
    ->dependsOn([Order::class])
    ->withSqlFragment('(SELECT COALESCE(SUM(amount), 0) FROM orders WHERE orders.user_id = {THIS}.id)')
    ->register();

⚠️ Reference the base table with the {THIS} placeholder. The engine substitutes {THIS} with the base model's compiled query alias at run time (e.g. orders.user_id = {THIS}.id), so the fragment stays correct wherever the model sits in the join β€” don't hard-code the table name.

Then your users just select it like a normal column:

->select(User::class, 'Lifetime Value', 'integer', isVirtual: true)

πŸ” Attribute-Level Security (ALS)

Beyond report assignment, the engine ships with a column-level security firewall. Restrict any attribute for a subject (a user, role, team, …) in one of two modes:

  • masked β€” the column may still be selected, but its values are redacted in the output.
  • blocked β€” the column is off-limits; using it in a filter, grouping, aggregate, or sort raises a ReportMakerSecurityException (blocked always wins over masked).
use App\Models\{User, Role};
use Nisalatp\DynamicReportGenerator\Facades\DynamicReport;

DynamicReport::restrictAttribute(User::class, 'salary', $supportRole, 'masked');  // redact
DynamicReport::restrictAttribute(User::class, 'ssn',    $supportRole, 'blocked'); // forbid

Enforcement is automatic at generation time. Pass the subjects explicitly, or let the engine resolve them from the authenticated user:

// explicit subjects
$rows = DynamicReport::generate($request, [$supportRole])->paginate(50);

// implicit β€” if your User implements DynamicReportSubject, the engine calls
// $user->getDynamicReportSubjects() for the logged-in user automatically
$rows = DynamicReport::generate($request)->paginate(50);

There's also model-level restriction β€” restrictModel(Model::class) hides an entire model from the report surface. Per-framework walkthroughs live in the Attribute-Level Security examples.

πŸ–ΌοΈ See it in action

All of the screens below run on the published engine β€” captured from the bundled iStore demo app.

Live results β€” the Data Preview returns real rows in-page, ready to export to CSV:

iStore β€” report results

Attribute-Level Security β€” a per-role column matrix; here email is Masked and password / remember_token are Blocked:

iStore β€” Attribute-Level Security matrix

AI-driven reporting (MCP) β€” ask a question in natural language and get a result set back, under the same security rules:

iStore β€” AI agent (Model Context Protocol)

πŸ”’ Security by design

  • Input is a typed JSON AST, validated at the boundary β€” raw user SQL never reaches the compiler.
  • All filter values are transmitted as PDO parameter bindings.
  • Aggregate functions are checked against an immutable whitelist (SUM, AVG, COUNT, MIN, MAX).

An independent review judged the engine "inherently secure against SQL injection."

🌐 Frontend-agnostic & AI-ready

One JSON AST drives every client β€” Blade/AlpineJS, Vue, React, a Java service. Because it's plain JSON, the AST is also natively compatible with the Model Context Protocol (MCP), so an LLM can turn a natural-language question straight into a valid report request. Runnable examples for each ecosystem live in the hosted docs, alongside the full AST reference.

βœ… Tested & validated

Covered by a PHPUnit 11 + Orchestra Testbench suite (112 tests, in-memory SQLite) spanning BFS join resolution, AST compilation, Virtual Attributes, Attribute-Level Security, SQL-injection binding and persistence. Independently, a third-party developer installed the published package into a clean Laravel app and ran the full suite β€” 112 of 112 passing, including 24/24 Attribute-Level Security tests β€” and reported it "a robust, production-ready reporting engine."

🀝 Contributing

This project is free and open-source. Issues and pull requests are welcome β€” whether that's NoSQL exploration, optimising the graph traversal, new frontend examples, or fixing a typo. Fork it, make your change, and open a PR.

πŸ“„ License

Open-sourced under the MIT license. Built by Nisala Aloka Bandara.