Skip to content
This repository was archived by the owner on Apr 1, 2024. It is now read-only.

Commit d00b3c4

Browse files
committed
Initial commit
0 parents  commit d00b3c4

28 files changed

Lines changed: 1253 additions & 0 deletions

.github/workflows/style.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: styling
2+
3+
on: [push]
4+
5+
jobs:
6+
style:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- name: Checkout code
11+
uses: actions/checkout@v2
12+
13+
- name: Run PHP CS Fixer
14+
uses: docker://oskarstark/php-cs-fixer-ga
15+
with:
16+
args: --config=tools/.php-cs-fixer.php --allow-risky=yes
17+
18+
- name: Extract branch name
19+
shell: bash
20+
run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
21+
id: extract_branch
22+
23+
- name: Commit changes
24+
uses: stefanzweifel/git-auto-commit-action@v2.3.0
25+
with:
26+
commit_message: Fix styling
27+
branch: ${{ steps.extract_branch.outputs.branch }}
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/tests.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: tests
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
phpunit:
7+
runs-on: ${{ matrix.os }}
8+
strategy:
9+
fail-fast: true
10+
matrix:
11+
os: [ubuntu-latest]
12+
php: [8.0]
13+
laravel: [8.*]
14+
dependency-version: [prefer-stable]
15+
include:
16+
- laravel: 8.*
17+
testbench: 6.*
18+
19+
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}
20+
21+
services:
22+
mysql:
23+
image: mysql:latest
24+
env:
25+
MYSQL_ALLOW_EMPTY_PASSWORD: yes
26+
MYSQL_DATABASE: testing
27+
ports:
28+
- 3306
29+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
30+
31+
steps:
32+
- name: Checkout code
33+
uses: actions/checkout@v2
34+
35+
- name: Cache composer dependencies
36+
uses: actions/cache@v1
37+
with:
38+
path: ~/.composer/cache/files
39+
key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
40+
41+
- name: Setup PHP
42+
uses: shivammathur/setup-php@v2
43+
with:
44+
php-version: ${{ matrix.php }}
45+
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick
46+
coverage: none
47+
48+
- name: Run composer
49+
run: |
50+
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
51+
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest
52+
53+
- name: Run tests
54+
run: vendor/bin/phpunit
55+
env:
56+
DB_PORT: ${{ job.services.mysql.ports[3306] }}

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
build
2+
composer.lock
3+
vendor
4+
storage
5+
tests/World/database.sqlite
6+
.DS_Store
7+
coverage
8+
.phpunit.result.cache
9+
.idea
10+
.php_cs.cache

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright © Matt Kingshott and contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
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

Comments
 (0)