|
| 1 | +<!-- Screenshot --> |
| 2 | +<p align="center"> |
| 3 | + <img src="resources/wallpaper.jpg" alt="Wallpaper"> |
| 4 | +</p> |
| 5 | + |
| 6 | +<!-- Badges --> |
| 7 | +<p align="center"> |
| 8 | + <img src="resources/version.svg" alt="Version"> |
| 9 | + <img src="resources/license.svg" alt="License"> |
| 10 | +</p> |
| 11 | + |
| 12 | +# Statistics |
| 13 | + |
| 14 | +This package enables a Laravel application to maintain statistics of aggregated database records. It serves as a companion package to (and relies upon) [triggers](https://github.com/mattkingshott/triggers). |
| 15 | + |
| 16 | +## Who is this for? |
| 17 | + |
| 18 | +If you're running queries that are slow because they need to perform aggregations (`COUNT`, `SUM`, `MIN`, `MAX` or `AVG`) across many records, then you might get some value from this package. A common scenario where this takes place, is on a dashboard that displays lots of statistics e.g. |
| 19 | + |
| 20 | +```sql |
| 21 | +SELECT |
| 22 | + (SELECT COUNT(*) FROM `articles`) AS 'articles', |
| 23 | + (SELECT COUNT(*) FROM `projects`) AS 'projects', |
| 24 | + (SELECT COUNT(*) FROM `tasks`) AS 'tasks' |
| 25 | +``` |
| 26 | + |
| 27 | +By contrast, you can configure the package to automatically maintain statistics in the background. So, instead of a slow query (like the above example), you can instead do this: |
| 28 | + |
| 29 | +```sql |
| 30 | +SELECT |
| 31 | + `table`, `values` |
| 32 | +FROM |
| 33 | + `statistics` |
| 34 | +WHERE |
| 35 | + `table` |
| 36 | +IN |
| 37 | + ('articles', 'projects', 'tasks') |
| 38 | +``` |
| 39 | + |
| 40 | +| Table | Values | |
| 41 | +| --------- | ----------------- | |
| 42 | +| Articles | `{ "count" : 6 }` | |
| 43 | +| Projects | `{ "count" : 3 }` | |
| 44 | +| Tasks | `{ "count" : 2 }` | |
| 45 | + |
| 46 | +## How does it work? |
| 47 | + |
| 48 | +The package will automatically register and migrate a `statistics` table to your database. This table then serves as a repository for aggregated values. You can then easily join records to this table to get the associated metrics you need. |
| 49 | + |
| 50 | +The aggregated values are maintained using database triggers, which will automatically fire after a record is inserted, updated or deleted. |
| 51 | + |
| 52 | +## Installation |
| 53 | + |
| 54 | +Pull in the package using Composer: |
| 55 | + |
| 56 | +```bash |
| 57 | +composer require mattkingshott/statistics |
| 58 | +``` |
| 59 | + |
| 60 | +## Configuration |
| 61 | + |
| 62 | +The package includes a configuration file that allows you to change the name of the database table that contains the aggregated values (the default is 'statistics'). If you want to change it, publish the configuration file using Artisan: |
| 63 | + |
| 64 | +```bash |
| 65 | +php artisan vendor:publish |
| 66 | +``` |
| 67 | + |
| 68 | +## Usage |
| 69 | + |
| 70 | +The package allows you to: |
| 71 | + |
| 72 | +1. Maintain statistics for all the records in a table. |
| 73 | +2. Maintain statistics for individual rows in a table. |
| 74 | +3. Maintain statistics for individual AND all records in a table. |
| 75 | + |
| 76 | +Before proceeding further, it is important to remember that database triggers (which the package relies on) can only be added to a table after it has been created. Therefore, you should ensure that any referenced tables have first been added via `Schema::create` within your migrations e.g. |
| 77 | + |
| 78 | +```php |
| 79 | +// At this point, adding statistics that rely on the 'users' table will |
| 80 | +// throw a SQL error as the table has not yet been added to the database. |
| 81 | + |
| 82 | +Schema::create('users', function(Blueprint $table) { |
| 83 | + // ... |
| 84 | +}); |
| 85 | + |
| 86 | +// At this point, adding statistics that rely on the 'users' table will not |
| 87 | +// throw a SQL error as the table has been added to the database. |
| 88 | +``` |
| 89 | + |
| 90 | +### Configuring models |
| 91 | + |
| 92 | +To begin, add the `InteractsWithStatistics` trait to any `Model` class that you want to maintain statistics for e.g. |
| 93 | + |
| 94 | +```php |
| 95 | +namespace App\Models; |
| 96 | + |
| 97 | +use Illuminate\Database\Eloquent\Model; |
| 98 | +use Statistics\Traits\InteractsWithStatistics; |
| 99 | + |
| 100 | +class Article extends Model |
| 101 | +{ |
| 102 | + use InteractsWithStatistics; |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +### Tracking all records |
| 107 | + |
| 108 | +To maintain statistics across the entire table (e.g. how many rows there are), call the static `trackAll` method on the `Model`. |
| 109 | + |
| 110 | +```php |
| 111 | +Article::trackAll(); |
| 112 | +``` |
| 113 | + |
| 114 | +Next, call one or more of the available aggregation methods: |
| 115 | + |
| 116 | +```php |
| 117 | +Article::trackAll() |
| 118 | + ->count() // Count all records |
| 119 | + ->sum('likes') // Get the sum of all records using the 'likes' column |
| 120 | + ->average('likes') // Get the average value from the 'likes' column |
| 121 | + ->minimum('likes') // Get the smallest value in the 'likes' column |
| 122 | + ->maximum('likes'); // Get the largest value in the 'likes' column |
| 123 | +``` |
| 124 | + |
| 125 | +You can call an aggregation method more than once if you need to maintain statistics on multiple columns. Simply supply a custom name to differentiate them: |
| 126 | + |
| 127 | +```php |
| 128 | +Article::trackAll() |
| 129 | + ->count() |
| 130 | + ->sum('likes', 'sum_likes') |
| 131 | + ->sum('views', 'sum_views'); |
| 132 | +``` |
| 133 | + |
| 134 | +Finally, call the `create` method to install the triggers. |
| 135 | + |
| 136 | +```php |
| 137 | +Article::trackAll() |
| 138 | + ->count() |
| 139 | + ->create(); |
| 140 | +``` |
| 141 | + |
| 142 | +#### Example |
| 143 | + |
| 144 | +Here's a simple example within a database migration: |
| 145 | + |
| 146 | +```php |
| 147 | +class CreateArticlesTable extends Migration |
| 148 | +{ |
| 149 | + public function up() : void |
| 150 | + { |
| 151 | + Schema::create('articles', function(Blueprint $table) { |
| 152 | + $table->unsignedTinyInteger('id'); |
| 153 | + $table->string('title'); |
| 154 | + }); |
| 155 | + |
| 156 | + Article::trackAll() |
| 157 | + ->count() |
| 158 | + ->create(); |
| 159 | + } |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +### Tracking individual rows |
| 164 | + |
| 165 | +In addition to maintaining statistics on a whole table, you can also maintain statistics for individual rows. This can be useful when tracking related records e.g. the count of 'articles', 'projects' and 'tasks' belonging to a 'user'. |
| 166 | + |
| 167 | +To begin, call the static `track` method on the `Model`: |
| 168 | + |
| 169 | +```php |
| 170 | +User::track(); |
| 171 | +``` |
| 172 | + |
| 173 | +Next, we need to add a watcher for each of the related tables. We do this by calling the `watch` method and supplying the related `Model`: |
| 174 | + |
| 175 | +```php |
| 176 | +User::track() |
| 177 | + ->watch(Task::class); |
| 178 | +``` |
| 179 | + |
| 180 | +The package will guess the foreign key on `Task` by combining the main model (`User`) and `_id`. However, you can override this by supplying your own foreign key: |
| 181 | + |
| 182 | +```php |
| 183 | +User::track() |
| 184 | + ->watch(Task::class, 'author_id'); |
| 185 | +``` |
| 186 | + |
| 187 | +Next, we need to provide an array of SQL statements that the trigger should execute in order to maintain one or more statistics: |
| 188 | + |
| 189 | +```php |
| 190 | +User::track() |
| 191 | + ->watch(Task::class, [ |
| 192 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 193 | + ]); |
| 194 | + |
| 195 | +// Or when supplying a custom foreign key... |
| 196 | + |
| 197 | +User::track() |
| 198 | + ->watch(Task::class, 'author_id', [ |
| 199 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `author_id` = {ROW}.author_id)", |
| 200 | + ]); |
| 201 | +``` |
| 202 | + |
| 203 | +Notice the use of `{ROW}` in the SQL statement. You can use this placeholder to access the current row within the trigger. `{ROW}` will be automatically replaced with `OLD` or `NEW` depending on the event type. |
| 204 | + |
| 205 | +You are free to maintain more than one statistic for a related table if required e.g. |
| 206 | + |
| 207 | +```php |
| 208 | +User::track() |
| 209 | + ->watch(Task::class, [ |
| 210 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 211 | + 'task_max_priority' => "(SELECT MAX(`priority`) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 212 | + ]); |
| 213 | +``` |
| 214 | + |
| 215 | +Next, repeat the process for any further watchers that you need e.g. |
| 216 | + |
| 217 | +```php |
| 218 | +User::track() |
| 219 | + ->watch(Task::class, [ |
| 220 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 221 | + 'task_max_priority' => "(SELECT MAX(`priority`) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 222 | + ]) |
| 223 | + ->watch(Project::class, [ |
| 224 | + 'project_count' => "(SELECT COUNT(*) FROM `projects` WHERE `user_id` = {ROW}.user_id)", |
| 225 | + ]) |
| 226 | + ->watch(Article::class, [ |
| 227 | + 'article_count' => "(SELECT COUNT(*) FROM `articles` WHERE `user_id` = {ROW}.user_id)", |
| 228 | + ]); |
| 229 | +``` |
| 230 | + |
| 231 | +Finally, call the `create` method to install the triggers. |
| 232 | + |
| 233 | +```php |
| 234 | +User::track() |
| 235 | + ->watch(Task::class, [ |
| 236 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 237 | + ]) |
| 238 | + ->create(); |
| 239 | +``` |
| 240 | + |
| 241 | +#### Example |
| 242 | + |
| 243 | +Here's a simple example within a database migration: |
| 244 | + |
| 245 | +```php |
| 246 | +return new class extends Migration |
| 247 | +{ |
| 248 | + public function up() : void |
| 249 | + { |
| 250 | + User::track() |
| 251 | + ->watch(Task::class, [ |
| 252 | + 'task_count' => "(SELECT COUNT(*) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 253 | + 'task_max_priority' => "(SELECT MAX(`priority`) FROM `tasks` WHERE `user_id` = {ROW}.user_id)", |
| 254 | + ]) |
| 255 | + ->watch(Project::class, [ |
| 256 | + 'project_count' => "(SELECT COUNT(*) FROM `projects` WHERE `user_id` = {ROW}.user_id)", |
| 257 | + ]) |
| 258 | + ->watch(Article::class, [ |
| 259 | + 'article_count' => "(SELECT COUNT(*) FROM `articles` WHERE `user_id` = {ROW}.user_id)", |
| 260 | + ]) |
| 261 | + ->create(); |
| 262 | + } |
| 263 | +}; |
| 264 | +``` |
| 265 | + |
| 266 | +## Contributing |
| 267 | + |
| 268 | +Thank you for considering a contribution to the package. You are welcome to submit a PR containing improvements, however if they are substantial in nature, please also be sure to include a test or tests. |
| 269 | + |
| 270 | +## Support the project |
| 271 | + |
| 272 | +If you'd like to support the development of the package, then please consider [sponsoring me](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YBEHLHPF3GUVY&source=url). Thanks so much! |
| 273 | + |
| 274 | +## License |
| 275 | + |
| 276 | +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. |
0 commit comments