<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Omar Essaouaf</title>
    <description>The latest articles on DEV Community by Omar Essaouaf (@omaressaouaf).</description>
    <link>https://dev.to/omaressaouaf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983149%2F86e6c106-ccab-42df-bc58-ea4a7341e985.png</url>
      <title>DEV Community: Omar Essaouaf</title>
      <link>https://dev.to/omaressaouaf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/omaressaouaf"/>
    <language>en</language>
    <item>
      <title>Building Cleaner Laravel Dashboard Statistics with Laravel Statistician</title>
      <dc:creator>Omar Essaouaf</dc:creator>
      <pubDate>Sun, 21 Jun 2026 19:07:39 +0000</pubDate>
      <link>https://dev.to/omaressaouaf/building-cleaner-laravel-dashboard-statistics-with-laravel-statistician-2jdc</link>
      <guid>https://dev.to/omaressaouaf/building-cleaner-laravel-dashboard-statistics-with-laravel-statistician-2jdc</guid>
      <description>&lt;h1&gt;
  
  
  Laravel package for generating clean and reusable dashboard
&lt;/h1&gt;

&lt;p&gt;Most dashboards start innocently: a card for total users, another one for total orders, then revenue, average order value, chart data, percentage change, and eventually caching because the dashboard is now doing too much work on every request.&lt;/p&gt;

&lt;p&gt;At first, this feels fine because each statistic is “just a query”. But as the application grows, the same logic starts appearing in controllers, services, widgets, reports, and API endpoints.&lt;/p&gt;

&lt;p&gt;That is the kind of small but annoying problem I wanted to solve with &lt;strong&gt;Laravel Statistician&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Laravel Statistician is a Laravel package for generating clean application statistics from Eloquent models, query builders, model classes, or plain table names.&lt;/p&gt;

&lt;p&gt;The goal is simple: keep dashboard and reporting logic reusable instead of duplicating query logic everywhere.&lt;/p&gt;

&lt;p&gt;Repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/omaressaouaf/laravel-statistician
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In many Laravel applications, dashboard statistics begin like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$totalUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$totalOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nv"&gt;$totalRevenue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$averageOrderValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is perfectly fine in the beginning. The problem appears later, when the dashboard needs date filters, chart data, percentage changes, cached numbers, and similar metrics reused in different places.&lt;/p&gt;

&lt;p&gt;At that point, the issue is no longer that the queries are difficult. The issue is that they are easy enough to duplicate.&lt;/p&gt;

&lt;p&gt;A controller has one version of the logic. An admin widget has another. A report export has another. An API endpoint has another.&lt;/p&gt;

&lt;p&gt;Over time, these small duplicated queries create inconsistency across the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Laravel Statistician Does
&lt;/h2&gt;

&lt;p&gt;Laravel Statistician provides a cleaner way to define and generate application statistics.&lt;/p&gt;

&lt;p&gt;It supports common dashboard needs such as aggregate statistics, date-range filtering, date-grouped statistics for charts, percentage changes between periods, trend statistics, multiple statistics in one call, caching, and custom keys.&lt;/p&gt;

&lt;p&gt;It is not trying to be a full analytics platform or a BI tool. It is a small package for the kind of statistics most Laravel applications need in dashboards, admin panels, reports, and internal tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;You can install it with Composer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require omaressaouaf/laravel-statistician
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Basic Aggregate Statistic
&lt;/h2&gt;

&lt;p&gt;Here is a simple example that counts users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Enums\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\AggregateSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\AggregateStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'users_count'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of writing the query directly inside a controller or dashboard widget, the statistic becomes a structured source. That makes it easier to reuse, extend, and compose with other statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Keys
&lt;/h2&gt;

&lt;p&gt;By default, the package generates a key based on the source and aggregate. You can also define your own key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'total_users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'total_users'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when you want your dashboard response to have clean and predictable names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Date Range Filtering
&lt;/h2&gt;

&lt;p&gt;Most dashboard statistics are not global. You usually want numbers for today, this week, this month, this year, or a custom date range.&lt;/p&gt;

&lt;p&gt;Laravel Statistician allows you to apply a start and end date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-12-31'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps date filtering consistent across different statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple Statistics in One Call
&lt;/h2&gt;

&lt;p&gt;A common dashboard pattern is returning several numbers together, such as users, orders, and revenue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Enums\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\AggregateSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\AggregateStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;keyBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'revenue'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$stats&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps related dashboard metrics grouped together instead of scattering them across multiple methods or services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sum, Average, Min, and Max
&lt;/h2&gt;

&lt;p&gt;You are not limited to counts. For example, you can calculate order statistics like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Enums\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\AggregateSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\AggregateStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'total'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be useful for ecommerce dashboards, SaaS admin panels, financial reports, and internal business tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Date-Grouped Statistics for Charts
&lt;/h2&gt;

&lt;p&gt;Dashboard charts usually need data grouped by date: users registered per day, orders per day, revenue per month, tickets created per week, and so on.&lt;/p&gt;

&lt;p&gt;Laravel Statistician includes date-grouped statistics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Enums\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\DateGroupedAggregateSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\DateGroupedAggregateStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DateGroupedAggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DateGroupedAggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-01-31'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'users_count_by_date'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'data'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date_label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'aggregate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'date_label'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'2025-01-02'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'aggregate'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;'date_format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Y-m-d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when building chart components because the data can be generated in a consistent format instead of being prepared manually every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Percentage Change
&lt;/h2&gt;

&lt;p&gt;Dashboards are usually more useful when they show context. “1,200 users” is useful, but “1,200 users, up 18% compared to the previous period” is much better.&lt;/p&gt;

&lt;p&gt;Laravel Statistician supports percentage change between periods:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\PercentageChangeSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\PercentageChangeStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PercentageChangeStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PercentageChangeSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-02-01'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2025-02-28'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'users_percentage_change'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps when building dashboard cards that compare current performance with a previous period.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Query Builders
&lt;/h2&gt;

&lt;p&gt;Sometimes a statistic is not based on an entire model. You may want to count only active users, completed orders, paid invoices, or filtered records.&lt;/p&gt;

&lt;p&gt;The package also works with query builders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Illuminate\Support\Facades\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Enums\Aggregate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Sources\AggregateSource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Omaressaouaf\LaravelStatistician\Statisticians\AggregateStatistician&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'users'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nc"&gt;Aggregate&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it flexible enough for real-world use cases where statistics often depend on business rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching Statistics
&lt;/h2&gt;

&lt;p&gt;Some dashboard statistics do not need to be recalculated on every request. Laravel Statistician supports caching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cacheFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also cache until a specific date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cacheUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;addDay&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And clear the cache conditionally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AggregateStatistician&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fromSources&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AggregateSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;clearCacheWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'refresh'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;cacheFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for dashboards where users expect fast responses, but the data does not need to be refreshed every second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;I like small packages that solve boring problems cleanly.&lt;/p&gt;

&lt;p&gt;Dashboard statistics are not usually the hardest part of a Laravel application, but they can easily make the codebase messy. The pattern often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Controller&lt;/span&gt;
&lt;span class="nv"&gt;$totalUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whereBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Somewhere else&lt;/span&gt;
&lt;span class="nv"&gt;$users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;whereDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;gt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Another dashboard widget&lt;/span&gt;
&lt;span class="nv"&gt;$userCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'created_at'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this is terrible alone. But over time, it creates inconsistency.&lt;/p&gt;

&lt;p&gt;Laravel Statistician is a small abstraction around this pattern. Not a heavy analytics system. Not a complex reporting engine. Just a cleaner way to define, reuse, cache, and return statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Philosophy
&lt;/h2&gt;

&lt;p&gt;The package follows a few simple principles.&lt;/p&gt;

&lt;p&gt;First, the API should stay easy to read. A developer should be able to understand what is being calculated without jumping through multiple files.&lt;/p&gt;

&lt;p&gt;Second, it should support common dashboard needs first: aggregates, date ranges, chart data, comparison stats, and caching.&lt;/p&gt;

&lt;p&gt;Third, it should work with existing Laravel patterns. The package supports Eloquent models, query builders, model classes, and table names instead of forcing a new data layer.&lt;/p&gt;

&lt;p&gt;Finally, it should stay extensible. Every application eventually has custom business metrics, so the package allows custom statisticians while preserving the same general developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  When This Package Is Useful
&lt;/h2&gt;

&lt;p&gt;Laravel Statistician can be useful for admin dashboards, SaaS dashboards, ecommerce reports, internal tools, CRM statistics, financial summaries, user activity reports, chart APIs, and KPI widgets.&lt;/p&gt;

&lt;p&gt;It is especially useful when multiple parts of the application need similar statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You May Not Need It
&lt;/h2&gt;

&lt;p&gt;You probably do not need this package if your dashboard has only one or two simple numbers, your statistics are extremely custom, you already use a dedicated analytics system, or your data is processed in a separate warehouse or BI tool.&lt;/p&gt;

&lt;p&gt;This package is meant for application-level statistics inside Laravel apps. It is not meant to replace full analytics infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Statistics logic is one of those things that feels too small to abstract at first. But once the dashboard grows, the duplication becomes obvious.&lt;/p&gt;

&lt;p&gt;Laravel Statistician is my attempt to make this part of Laravel applications cleaner: define statistics once, reuse them, group them, cache them, and return them in a predictable format.&lt;/p&gt;

&lt;p&gt;The package is open source, and I would love feedback from Laravel developers.&lt;/p&gt;

&lt;p&gt;Repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/omaressaouaf/laravel-statistician
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build dashboards in Laravel, I hope this package saves you from writing the same statistics queries again and again.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>opensource</category>
      <category>statistics</category>
    </item>
    <item>
      <title>What Phone Sanitization Revealed About Our Data</title>
      <dc:creator>Omar Essaouaf</dc:creator>
      <pubDate>Sat, 13 Jun 2026 20:23:00 +0000</pubDate>
      <link>https://dev.to/omaressaouaf/what-phone-sanitization-revealed-about-our-data-11da</link>
      <guid>https://dev.to/omaressaouaf/what-phone-sanitization-revealed-about-our-data-11da</guid>
      <description>&lt;h2&gt;
  
  
  I Thought This Would Take a Day
&lt;/h2&gt;

&lt;p&gt;The task: normalize phone numbers across a sharded production system. Multiple country databases, years of inconsistent formats from different integrations, the usual archaeology. I figured I'd write a script Monday, handle edge cases Tuesday, open a PR Wednesday.&lt;/p&gt;

&lt;p&gt;It took the whole week. Not because phone normalization is hard, but because the problem kept exposing things I hadn't thought about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework I didn't need
&lt;/h2&gt;

&lt;p&gt;When I read the schema I saw patterns everywhere. Phone columns scattered across dozens of tables with different names, different formats, different roles in the data model. My brain immediately went to: &lt;em&gt;this should be generic&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Within a few hours I had a design for dynamic schema discovery, automatic phone column detection, a table classification layer, configurable skip policies, dynamic country resolution per shard. The kind of system that could handle any similar migration in any future codebase.&lt;/p&gt;

&lt;p&gt;I started building it. The abstractions felt justified. Then I asked myself: why does this actually need to be general?&lt;/p&gt;

&lt;p&gt;It didn't. This was a maintenance command that would run once, maybe twice, against one specific system I understood. The reusability I was designing for was completely hypothetical. YAGNI applies to migrations too, maybe more so, because the cost of wrong abstractions in a migration isn't just complexity, it's also the surface area for subtle bugs in code that touches production data.&lt;/p&gt;

&lt;p&gt;I deleted most of it.&lt;/p&gt;

&lt;p&gt;The final command was explicit about everything: scan the &lt;code&gt;accounts&lt;/code&gt; table in each country shard, normalize the phone fields used for identity, detect duplicates using a temporary SQL table, store the conflicting account IDs, normalize phone columns in downstream tables, skip rows belonging to duplicate accounts since those need human review, produce a structured JSON report. Every table name in the config. Every column name spelled out. No dynamic resolution, no introspection.&lt;/p&gt;

&lt;p&gt;It felt almost embarrassingly simple. Then it started feeling right.&lt;/p&gt;

&lt;p&gt;There's a principle I keep running into that I'd call the maintenance command corollary: tools that run infrequently and touch sensitive data should optimize for auditability over extensibility. The engineer running this six months from now, under pressure, needs to be able to read it and trust it quickly. They don't need it to be clever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalization changes identity
&lt;/h2&gt;

&lt;p&gt;This was the part I hadn't fully thought through, and it reframed the entire project.&lt;/p&gt;

&lt;p&gt;Two phone numbers that look different can represent the same subscriber. &lt;code&gt;+1 (800) 555-0123&lt;/code&gt; and &lt;code&gt;18005550123&lt;/code&gt; are the same number once you apply E.164 normalization. When you collapse those representations across millions of account records, accounts that have been treated as distinct for years suddenly resolve to the same person.&lt;/p&gt;

&lt;p&gt;We had duplicates nobody knew about. They had existed quietly in the system across multiple shards because no one had ever compared canonical forms at scale. The normalization didn't create the duplicates, it just made them visible for the first time.&lt;/p&gt;

&lt;p&gt;This is the thing about data migrations that makes them genuinely different from feature work. A feature has an off switch. A migration that has run against production data usually doesn't. If you normalize a phone number and discard the original, you've made a permanent decision. If your normalization logic has a bug, you may not be able to reconstruct what the original value was. The blast radius of a wrong decision is bounded in different ways than application bugs.&lt;/p&gt;

&lt;p&gt;This is also why duplicate detection ended up being the majority of the actual work. The normalization logic was straightforward. The harder problem was: once we find accounts that now share a canonical phone number, what do we do? Automatically merge? Pick one? The answer was neither: surface them in the report and skip any downstream normalization that touches those accounts until a human reviews them. Automation should stop at the boundary of decisions it doesn't have enough context to make.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pushing aggregation into the database
&lt;/h2&gt;

&lt;p&gt;My first instinct for duplicate detection was in-process: stream records, build a map of canonical phone to account IDs, flag any key with more than one entry. Reasonable approach, and for small datasets it would have been fine.&lt;/p&gt;

&lt;p&gt;The problem was scale combined with the shard architecture. Some country databases have tens of millions of accounts. Holding all canonical phone values in memory simultaneously, across a migration that needed to be resumable, was going to be awkward and wasteful.&lt;/p&gt;

&lt;p&gt;What I did instead: buffer inserts of &lt;code&gt;(canonical_phone, account_id)&lt;/code&gt; tuples into a temporary table, flush in batches, then run a single &lt;code&gt;GROUP BY canonical_phone HAVING COUNT(*) &amp;gt; 1&lt;/code&gt; at the end. The database does the aggregation, uses its own indexes, and produces exactly the set I need. Total memory footprint on the application side stays flat regardless of table size.&lt;/p&gt;

&lt;p&gt;It's not a novel pattern. But I notice engineers, myself included, sometimes reach for in-process solutions out of habit even when the data is already in a database that is extremely good at this specific class of problem. The embarrassing version of this mistake is reimplementing GROUP BY in a hash map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same column type, different semantics
&lt;/h2&gt;

&lt;p&gt;I went in assuming I could treat phone numbers uniformly. They're all phone numbers. Normalize them all the same way. That assumption broke down quickly.&lt;/p&gt;

&lt;p&gt;Some phone fields are identity fields. They are how the system finds and matches accounts. Normalizing them is what drives the deduplication logic. Others are contact preferences or communication records that happen to store a phone number but play no role in identity resolution. Others live in event logs that represent historical facts and probably shouldn't be rewritten at all.&lt;/p&gt;

&lt;p&gt;The data type is the same. The semantics are completely different, and the right migration behavior depends on the semantics, not the type.&lt;/p&gt;

&lt;p&gt;This also came up in a subtler way when I was writing the config schema. I had a field called &lt;code&gt;owner_uid_column&lt;/code&gt; because I was thinking about the mechanism: this table has a foreign key to the account UID. But what I actually meant was "the column that tells me which account owns this row." I renamed it to &lt;code&gt;owner_column&lt;/code&gt;. Small change, but the original name encoded an assumption about key type that wasn't relevant to what the config was trying to express. Config that describes intent ages better than config that describes implementation.&lt;/p&gt;

&lt;p&gt;The broader principle here is that column relationships don't imply identical business rules. Two tables can both have a foreign key to the same parent entity and still require completely different handling in a migration. The relationship tells you where data comes from, not what it means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behavioral equivalence as a refactoring strategy
&lt;/h2&gt;

&lt;p&gt;I refactored this several times over the week, removing abstractions in each pass. The thing that made that safe was a specific validation approach: I compared structured outputs between versions rather than reviewing code.&lt;/p&gt;

&lt;p&gt;After each refactor, I ran both versions against the same dataset and diffed the results: duplicate groups found, account IDs flagged, changed rows, unchanged rows, skipped rows, invalid rows, the final report structure. They had to be byte-for-byte identical. Not functionally similar, identical.&lt;/p&gt;

&lt;p&gt;This is a stronger guarantee than code review. Code review catches things that look wrong. Output comparison catches things that are wrong. The implementation can change arbitrarily as long as the observable behavior doesn't change. That's the actual definition of a correct refactor, and having a machine verify it rather than a human read it is more reliable.&lt;/p&gt;

&lt;p&gt;It also changes the psychology of simplification. Deleting an abstraction feels risky when you're relying on your own judgment that the replacement is equivalent. It feels much less risky when you have a diff that shows zero behavioral difference. I deleted a few hundred lines this way that I wouldn't have been confident deleting based on reading alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this landed
&lt;/h2&gt;

&lt;p&gt;The version I shipped was smaller and more boring than what I had on Tuesday. Every table explicit. Every column explicit. Config that reads like a description of the business problem rather than a description of the implementation. A report that tells you exactly what happened, row by row, so the person running it next time can audit the results without reading the code.&lt;/p&gt;

&lt;p&gt;I've been writing software long enough to know that "boring" and "correct" are not in tension. For a migration tool that runs occasionally and touches data you can't easily recover, boring is exactly what you want.&lt;/p&gt;

</description>
      <category>database</category>
      <category>data</category>
      <category>normalization</category>
      <category>phone</category>
    </item>
  </channel>
</rss>
