Skip to content

Commit 07b94de

Browse files
authored
Merge pull request #24 from DirectoryTree/bug-21
Properly handle JSON `additional` attributes for metrics
2 parents 75f6f05 + a7fffeb commit 07b94de

File tree

4 files changed

+89
-6
lines changed

4 files changed

+89
-6
lines changed

.github/workflows/run-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
os: [ubuntu-latest]
18-
php: [8.1, 8.2, 8.3, 8.4]
18+
php: [8.1, 8.2, 8.3, 8.4, 8.5]
1919
include:
20-
- php: 8.4
20+
- php: 8.5
2121
laravel: 13.*
2222
testbench: 11.*
2323
pest: 4.*

src/Jobs/RecordMetric.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use DirectoryTree\Metrics\Metric;
88
use Illuminate\Bus\Queueable;
99
use Illuminate\Contracts\Queue\ShouldQueue;
10+
use Illuminate\Database\Eloquent\Model;
1011
use Illuminate\Foundation\Bus\Dispatchable;
1112
use Illuminate\Support\Collection;
1213

@@ -38,12 +39,12 @@ public function handle(): void
3839
fn (Measurable $metric) => $metric->value()
3940
);
4041

41-
/** @var \Illuminate\Database\Eloquent\Model $model */
42+
/** @var Model $model */
4243
$model = transform($metric->model() ?? DatabaseMetricManager::$model, fn (string $model) => new $model);
4344

4445
$model->getConnection()->transaction(function () use ($metric, $value, $model) {
4546
$instance = $model->newQuery()->firstOrCreate([
46-
...$metric->additional(),
47+
...$this->getAdditionalAttributes($metric, $model),
4748
'name' => $metric->name(),
4849
'category' => $metric->category(),
4950
'year' => $metric->year(),
@@ -59,4 +60,17 @@ public function handle(): void
5960
->increment('value', $value);
6061
});
6162
}
63+
64+
/**
65+
* Get the additional attributes for the metric.
66+
*/
67+
protected function getAdditionalAttributes(Measurable $metric, Model $model): array
68+
{
69+
return Collection::make($metric->additional())->mapWithKeys(fn (mixed $value, mixed $key) => [
70+
// If the model has a cast for the key, we can assume the model will
71+
// handle the value correctly. If not, and the value is an array,
72+
// we should encode it as JSON before attempting to store it.
73+
$key => $model->hasCast($key) ? $value : (is_array($value) ? json_encode($value) : $value),
74+
])->all();
75+
}
6276
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
use DirectoryTree\Metrics\Jobs\RecordMetric;
4+
use DirectoryTree\Metrics\Metric;
5+
use DirectoryTree\Metrics\MetricData;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
beforeEach(function () {
10+
Metric::query()->delete();
11+
12+
if (! Schema::hasColumn('metrics', 'payload')) {
13+
Schema::table('metrics', function (Blueprint $table) {
14+
$table->json('payload');
15+
});
16+
}
17+
});
18+
19+
afterEach(function () {
20+
if (Schema::hasColumn('metrics', 'payload')) {
21+
Schema::table('metrics', function (Blueprint $table) {
22+
$table->dropColumn('payload');
23+
});
24+
}
25+
});
26+
27+
it('creates metrics with json payload attributes', function () {
28+
$data = new MetricData('page_views_with_json', additional: [
29+
'payload' => [
30+
'a' => 1,
31+
'b' => 'test',
32+
],
33+
]);
34+
35+
(new RecordMetric($data))->handle();
36+
(new RecordMetric($data))->handle();
37+
38+
// Count may be 1 (SQLite dedupes by JSON string) or 2 (MySQL compares JSON differently).
39+
expect(Metric::where('name', 'page_views_with_json')->count())->toBeGreaterThanOrEqual(1);
40+
41+
$metric = Metric::where('name', 'page_views_with_json')->first();
42+
43+
// Cast the payload attribute manually since we added the column dynamically
44+
$metric->mergeCasts(['payload' => 'array']);
45+
46+
expect($metric->payload)->toBe(['a' => 1, 'b' => 'test']);
47+
48+
// Total value across all matching records should equal 2 regardless of DB.
49+
expect(Metric::where('name', 'page_views_with_json')->sum('value'))->toEqual(2);
50+
});
51+
52+
it('differentiates metrics by json payload content', function () {
53+
$data1 = new MetricData('page_views_by_source', additional: [
54+
'payload' => ['source' => 'google'],
55+
]);
56+
57+
$data2 = new MetricData('page_views_by_source', additional: [
58+
'payload' => ['source' => 'facebook'],
59+
]);
60+
61+
(new RecordMetric($data1))->handle();
62+
(new RecordMetric($data2))->handle();
63+
64+
// Count may be 2 (SQLite) or more (MySQL) depending on JSON comparison behavior.
65+
expect(Metric::where('name', 'page_views_by_source')->count())->toBeGreaterThanOrEqual(2);
66+
});

tests/TestCase.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
namespace DirectoryTree\Metrics\Tests;
44

55
use DirectoryTree\Metrics\MetricServiceProvider;
6+
use Illuminate\Foundation\Testing\RefreshDatabase;
67
use Orchestra\Testbench\TestCase as BaseTestCase;
78

8-
use function Orchestra\Testbench\laravel_migration_path;
9+
use function Orchestra\Testbench\default_migration_path;
910

1011
abstract class TestCase extends BaseTestCase
1112
{
13+
use RefreshDatabase;
14+
1215
/**
1316
* Define database migrations.
1417
*/
1518
protected function defineDatabaseMigrations(): void
1619
{
17-
$this->loadMigrationsFrom(laravel_migration_path('/'));
20+
$this->loadMigrationsFrom(default_migration_path('/'));
1821
$this->loadMigrationsFrom(__DIR__.'/../database/migrations');
1922
}
2023

0 commit comments

Comments
 (0)