Skip to content

Commit 07ff48c

Browse files
committed
feat: enhance BizException with logging capabilities and translation parameters
1 parent 1a7d4db commit 07ff48c

File tree

5 files changed

+230
-26
lines changed

5 files changed

+230
-26
lines changed

app/Exceptions/BizException.php

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,100 @@
66

77
use Exception;
88
use Illuminate\Http\JsonResponse;
9-
use Illuminate\Http\Request;
109
use Illuminate\Support\Facades\Log;
10+
use Illuminate\Support\Str;
1111

1212
class BizException extends Exception
1313
{
14-
public function __construct(
15-
string $message = '',
16-
protected string $logMessage = '',
17-
protected array $context = [],
18-
int $code = 400,
19-
?Exception $previous = null)
14+
protected string $logMessage = '';
15+
protected array $logContext = [];
16+
protected array $transParams = [];
17+
18+
public static function make(string $message): self
2019
{
21-
parent::__construct($message, $code, $previous);
20+
return new static($message);
2221
}
2322

24-
public function setFile(string $file): void
23+
public function code(int $code): self
2524
{
26-
$this->file = $file;
25+
$this->code = $code;
26+
return $this;
2727
}
2828

29-
public function setLine(int $line): void
29+
public function with(array|string $key, $value = null): self
3030
{
31-
$this->line = $line;
31+
if (is_array($key)) {
32+
$this->transParams = [...$this->transParams, ...$key];
33+
} else {
34+
$this->transParams[$key] = $value;
35+
}
36+
return $this;
3237
}
3338

34-
public function report(): void
39+
public function logMessage(string $message)
40+
{
41+
$this->logMessage = $message;
42+
return $this;
43+
}
44+
45+
public function logContext(array $context): self
3546
{
36-
$previousTrace = $this->getPrevious();
37-
$previousTrace = $this->getPrevious() ? explode("\n", $this->getPrevious()->getTraceAsString()) : [];
38-
$previousTraceSlice = array_slice($previousTrace, 0, 5);
39-
$previousTraceString = implode("\n", $previousTraceSlice);
47+
$this->logContext = $context;
48+
return $this;
49+
}
4050

41-
$logEntry = [
42-
'message' => $this->logMessage,
43-
'context' => $this->context,
51+
public function report(): void
52+
{
53+
$logData = [
54+
'client_params' => $this->transParams,
55+
'log_context' => $this->logContext,
4456
'file' => $this->getFile(),
4557
'line' => $this->getLine(),
46-
'previous' => $previousTraceString,
58+
'filtered_trace' => $this->getFilteredTrace(),
4759
];
48-
Log::info('BizException', $logEntry);
60+
61+
Log::channel('biz')->error($this->getLogMessage(), $logData);
62+
}
63+
64+
private function getFilteredTrace(): array
65+
{
66+
return collect($this->getTrace())
67+
->filter(fn ($trace) => $this->isAppTrace($trace))
68+
->take(5)
69+
->map(fn ($trace) => [
70+
'file' => Str::after($trace['file'], base_path()),
71+
'line' => $trace['line'] ?? 0,
72+
'caller' => $this->formatCaller($trace)
73+
])
74+
->all();
75+
}
76+
77+
private function isAppTrace(array $trace): bool
78+
{
79+
$file = $trace['file'] ?? '';
80+
81+
return Str::startsWith($file, base_path().'/contexts');
82+
}
83+
84+
private function formatCaller(array $trace): string
85+
{
86+
$class = $trace['class'] ?? '';
87+
$type = $trace['type'] ?? '';
88+
$function = $trace['function'] ?? '';
89+
90+
return $class ? "$class$type$function()" : $function.'()';
91+
}
92+
93+
private function getLogMessage(): string
94+
{
95+
return $this->logMessage ?: "[BizError] {$this->message}";
4996
}
5097

51-
public function render(Request $request): JsonResponse
98+
public function render(): JsonResponse
5299
{
53100
return response()->json([
54101
'success' => false,
55-
'message' => $this->message,
56-
], $this->getCode());
102+
'message' => trans($this->message, $this->transParams)
103+
], $this->code ?: 400);
57104
}
58105
}

config/logging.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,14 @@
5454

5555
'channels' => [
5656

57+
'biz' => [
58+
'driver' => 'daily',
59+
'path' => storage_path('logs/biz/biz.log'),
60+
'level' => 'error',
61+
'days' => env('LOG_DAILY_DAYS', 14),
62+
'replace_placeholders' => true,
63+
],
64+
5765
'stack' => [
5866
'driver' => 'stack',
5967
'channels' => explode(',', env('LOG_STACK', 'single')),

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</testsuite>
1212
<testsuite name="Feature">
1313
<directory>contexts/*/Tests/Feature</directory>
14-
<!-- <directory>tests/Feature</directory> -->
14+
<directory>tests/Feature</directory>
1515
</testsuite>
1616
<testsuite name="Shared">
1717
<directory>tests/Unit</directory>
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use App\Exceptions\BizException;
6+
use Illuminate\Support\Facades\Log;
7+
use Illuminate\Support\Str;
8+
use Illuminate\Http\JsonResponse;
9+
10+
beforeEach(function () {
11+
$this->exception = BizException::make('test.message');
12+
});
13+
14+
it('can be created using make method', function () {
15+
$exception = BizException::make('test.message');
16+
17+
expect($exception)->toBeInstanceOf(BizException::class)
18+
->and($exception->getMessage())->toBe('test.message');
19+
});
20+
21+
it('can set error code', function () {
22+
$this->exception->code(422);
23+
24+
expect($this->exception->getCode())->toBe(422);
25+
});
26+
27+
it('can add translation parameters using with method with key-value pair', function () {
28+
$this->exception->with('name', 'John');
29+
30+
$reflectionClass = new ReflectionClass($this->exception);
31+
$property = $reflectionClass->getProperty('transParams');
32+
$property->setAccessible(true);
33+
34+
expect($property->getValue($this->exception))->toBe(['name' => 'John']);
35+
});
36+
37+
it('can add translation parameters using with method with array', function () {
38+
$this->exception->with(['name' => 'John', 'age' => 30]);
39+
40+
$reflectionClass = new ReflectionClass($this->exception);
41+
$property = $reflectionClass->getProperty('transParams');
42+
$property->setAccessible(true);
43+
44+
expect($property->getValue($this->exception))->toBe(['name' => 'John', 'age' => 30]);
45+
});
46+
47+
it('can set log message', function () {
48+
$this->exception->logMessage('Custom log message');
49+
50+
$reflectionClass = new ReflectionClass($this->exception);
51+
$property = $reflectionClass->getProperty('logMessage');
52+
$property->setAccessible(true);
53+
54+
expect($property->getValue($this->exception))->toBe('Custom log message');
55+
});
56+
57+
it('can set log context', function () {
58+
$context = ['user_id' => 1, 'action' => 'create'];
59+
$this->exception->logContext($context);
60+
61+
$reflectionClass = new ReflectionClass($this->exception);
62+
$property = $reflectionClass->getProperty('logContext');
63+
$property->setAccessible(true);
64+
65+
expect($property->getValue($this->exception))->toBe($context);
66+
});
67+
68+
it('reports to log channel', function () {
69+
Log::shouldReceive('channel')
70+
->once()
71+
->with('biz')
72+
->andReturnSelf();
73+
74+
Log::shouldReceive('error')
75+
->once()
76+
->withArgs(function ($message, $data) {
77+
return $message === '[BizError] test.message' &&
78+
isset($data['client_params']) &&
79+
isset($data['log_context']) &&
80+
isset($data['file']) &&
81+
isset($data['line']) &&
82+
isset($data['filtered_trace']);
83+
});
84+
85+
$this->exception->report();
86+
});
87+
88+
it('renders as JSON response', function () {
89+
$this->exception->code(422)->with('param', 'value');
90+
91+
$response = $this->exception->render();
92+
93+
expect($response)->toBeInstanceOf(JsonResponse::class)
94+
->and($response->getStatusCode())->toBe(422)
95+
->and(json_decode($response->getContent(), true))->toBe([
96+
'success' => false,
97+
'message' => 'test.message'
98+
]);
99+
});
100+
101+
it('uses default error code 400 when no code set', function () {
102+
// Mock the trans function
103+
$this->mock('alias:trans', function ($mock) {
104+
$mock->shouldReceive('__invoke')
105+
->with('test.message', [])
106+
->andReturn('Translated message');
107+
});
108+
109+
$response = $this->exception->render();
110+
111+
expect($response->getStatusCode())->toBe(400);
112+
});
113+
114+
it('uses custom log message when available', function () {
115+
$this->exception->logMessage('Custom log message');
116+
117+
$reflectionClass = new ReflectionClass($this->exception);
118+
$method = $reflectionClass->getMethod('getLogMessage');
119+
$method->setAccessible(true);
120+
121+
expect($method->invoke($this->exception))->toBe('Custom log message');
122+
});
123+
124+
it('formats caller correctly with class type and function', function () {
125+
$reflectionClass = new ReflectionClass($this->exception);
126+
$method = $reflectionClass->getMethod('formatCaller');
127+
$method->setAccessible(true);
128+
129+
$trace = [
130+
'class' => 'TestClass',
131+
'type' => '::',
132+
'function' => 'testMethod'
133+
];
134+
135+
expect($method->invoke($this->exception, $trace))->toBe('TestClass::testMethod()');
136+
});
137+
138+
it('formats caller correctly with only function', function () {
139+
$reflectionClass = new ReflectionClass($this->exception);
140+
$method = $reflectionClass->getMethod('formatCaller');
141+
$method->setAccessible(true);
142+
143+
$trace = [
144+
'function' => 'testMethod'
145+
];
146+
147+
expect($method->invoke($this->exception, $trace))->toBe('testMethod()');
148+
});

tests/Pest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
uses(Tests\TestCase::class, RefreshDatabase::class)->in(__DIR__.'/../contexts/*/Tests/Feature');
19+
uses(Tests\TestCase::class, RefreshDatabase::class)->in(__DIR__.'/../tests/Feature');
1920

2021
/*
2122
|--------------------------------------------------------------------------

0 commit comments

Comments
 (0)