r/PHP 7h ago

GitHub - eznix86/laravel-analytics: Data Build Tool the eloquent way

https://github.com/eznix86/laravel-analytics

At work, I kept writing the same thing: a few analytics tables, a cron job to rebuild them, and the same SQL copy-pasted into three models. Change one definition, forget to update one copy, and suddenly two dashboards quietly disagree.

There’s `dbt`, which solves this problem for data teams, but it means bringing Python and a second toolchain into a Laravel project. So I tried the same idea in PHP.

An analytics model is an Eloquent model with one query on it:

```php
class Revenue extends Model implements AnalyticsModel
{
use Analytics;

public function computes(): Query
{
return $this->from(Order::class)
->where('status', '<>', 'cancelled')
->per('customer_id')
->measure('total', 'sum(amount)');
}
}
```

Then `php artisan analytics:sync` works out what depends on what and builds everything in the right order.

After that, it’s just Eloquent:

```php
Revenue::query()->where('total', '>', 1000)->get();
```

A few things it does:

* The `GROUP BY` comes from the dimensions you declare, so you never have to write them twice.
* it has Incremental, microbatch, and snapshot buildsthe, same ideas as dbt.
* Runs on PostgreSQL, MySQL, and SQLite with the same commands.

### What it does not do

Every model in a dependency chain has to use the same connection.

That means you can’t, for example, import a SQLite query directly into a PostgreSQL query. This could be solved with an import mechanism, and I am still thinking about a better way to make that work in an Eloquent-like way.

### Why not just use a query class with dependency injection?

A query class, like the action pattern in `App\Queries`, that you inject wherever you need it is perfectly fine.

If the aggregate is fast, you need live numbers, and you only have one or two of them, write the class and skip this package.

The issue is that it computes on every read and you have zero indexes.

Cache the query? Now you’re stuck dealing with stale data.

There’s another problem: each layer (CTEs, subqueries, etc.) gets re-run instead of being reused.

You can use query classes can be composable by calling each other, but a shared subquery is still recomputed inside every caller. This package composes by reference.

For example, you can have a `StgOrder` model representing a transformed version of the `Order` table. It gets built once, and the models that depend on it simply select from the finished table.

This package can append the rows that arrived since the last run, rebuild one day at a time, or keep one row per version with `valid_from` and `valid_to`.

This package will make a built table that can carry the indexes your read patterns need.

A helper like `Revenue::isStale()` can tell you when the data has passed its freshness window.

### Why not just write a job that rebuilds the table?

That’s essentially what the package does.

The difference is that the queries are reusable, and dependencies are propagated through the entire chain of downstream aggregates.

TLDR; You write reusable queries as a data person but in PHP.

Repo: https://github.com/eznix86/laravel-analytics

Read more about DBT: https://en.wikipedia.org/wiki/Data_build_tool

the real dbt guys: https://github.com/dbt-labs/dbt-core (for the curious folks)

0 Upvotes

3 comments sorted by

1

u/kantorcodes1 7h ago

does analytics:sync --full-refresh guarantee one generation across the whole dependency graph, or only atomic swaps per model? e.g. can an upstream table be on the new build while a downstream model is still from the previous run?

1

u/Eznix86 5h ago edited 5h ago

Hey, btw, thanks for checking it out. So the current behavior is atomic at the model level: a table/view flips from the old build to the new build without being missing or half-written.

But it does NOT guarantee one generation across the whole dependency graph. In particular, with incremental, microbatch, or snapshot models, an upstream model could be on the new generation while a downstream model is still on the previous one.

I'm trying to stay closer to dbt's approach here. A true graph-wide generation swap is straightforward for fully rebuilt table/view/ephemeral graphs, but not for incremental/microbatch models because their state lives in the existing table.

Snowflake, BigQuery, and Databricks can get around this with zero-copy clones, which is something I don't feel to support yet (because YAGNI? for my use case). I'm still looking at what the equivalent could be across Postgres, MySQL, and SQLite.

And open for suggestions :)

Edit:

If you do not want to hook it with a real project, i made a demo: https://github.com/eznix86/laravel-analytics-demo that you can break, need sqlite, postgres and mysql.

1

u/kantorcodes1 4h ago

that answer makes the Laravel-specific part pretty simple: analytics:sync mutates the analytics relations, and --full-refresh is the much heavier path. i work on HOL Guard, an open-source local check before agent-run commands execute. native support would let coding agents get Laravel Analytics-aware handling instead of treating both as generic php artisan. would you be up for contributing the extension?