Logging
This commit is contained in:
parent
f1d46dacb6
commit
75ce822b84
6 changed files with 399 additions and 1 deletions
|
|
@ -1,5 +1,4 @@
|
|||
parameters:
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
level: 5
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ trait DataObject
|
|||
*/
|
||||
public static function fromArray(array $input): ?static
|
||||
{
|
||||
$logger = new Log();
|
||||
$parameters = RuleFactory::getParametersMeta(static::class);
|
||||
foreach ($parameters as $parameter) {
|
||||
$parameterName = $parameter->reflection->getName();
|
||||
|
|
@ -66,12 +67,15 @@ trait DataObject
|
|||
continue;
|
||||
}
|
||||
}
|
||||
$logger->inputRaw($input);
|
||||
|
||||
$rules = static::getRules();
|
||||
$logger->rules($rules);
|
||||
|
||||
$validator = static::withValidator($input, $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$logger->validationErrors($validator->errors()->toArray());
|
||||
return static::fails($validator);
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +98,7 @@ trait DataObject
|
|||
$parameter->reflection,
|
||||
);
|
||||
}
|
||||
$logger->input($mappedInput);
|
||||
return App::make(static::class, $mappedInput);
|
||||
}
|
||||
|
||||
|
|
|
|||
67
src/Log.php
Normal file
67
src/Log.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
readonly class Log
|
||||
{
|
||||
public LoggerInterface $logger;
|
||||
public function __construct()
|
||||
{
|
||||
$raw = config('dto.log.logger');
|
||||
if (is_callable($raw)) {
|
||||
$this->logger = App::call($raw);
|
||||
return;
|
||||
}
|
||||
if (is_object($raw)) {
|
||||
$this->logger = $raw;
|
||||
return;
|
||||
}
|
||||
if (is_string($raw) && class_exists($raw)) {
|
||||
$this->logger = App::make($raw);
|
||||
return;
|
||||
}
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<int, string|Rule>> $rules
|
||||
*/
|
||||
public function rules(array $rules): void
|
||||
{
|
||||
$level = config('dto.log.rules') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($rules, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $input
|
||||
*/
|
||||
public function input(array $input): void
|
||||
{
|
||||
$level = config('dto.log.input') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($input, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, null|int|float|string|array> $input
|
||||
*/
|
||||
public function inputRaw(array $input): void
|
||||
{
|
||||
$level = config('dto.log.raw_input') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($input, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, string>> $errors
|
||||
*/
|
||||
public function validationErrors(array $errors): void
|
||||
{
|
||||
$level = config('dto.log.validation_errors') ?? LogLevel::INFO;
|
||||
$this->logger->log($level, print_r($errors, true));
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,20 @@
|
|||
|
||||
use Icefox\DTO\Factories\CollectionFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
return [
|
||||
'cast' => [],
|
||||
'rules' => [
|
||||
Collection::class => CollectionFactory::rules(...),
|
||||
],
|
||||
'log' => [
|
||||
'logger' => NullLogger::class,
|
||||
'internal' => LogLevel::WARNING,
|
||||
'rules' => LogLevel::DEBUG,
|
||||
'input' => LogLevel::DEBUG,
|
||||
'raw_input' => LogLevel::DEBUG,
|
||||
'validation_errors' => LogLevel::INFO,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
36
tests/Logging/CustomLogger.php
Normal file
36
tests/Logging/CustomLogger.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Logging;
|
||||
|
||||
use Psr\Log\AbstractLogger;
|
||||
|
||||
class CustomLogger extends AbstractLogger
|
||||
{
|
||||
public array $logs = [];
|
||||
|
||||
public function log($level, string|\Stringable $message, array $context = []): void
|
||||
{
|
||||
$this->logs[] = [
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
];
|
||||
}
|
||||
|
||||
public function hasLog(string $level, string $contains): bool
|
||||
{
|
||||
foreach ($this->logs as $log) {
|
||||
if ($log['level'] === $level && str_contains($log['message'], $contains)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->logs = [];
|
||||
}
|
||||
}
|
||||
281
tests/Logging/LogTest.php
Normal file
281
tests/Logging/LogTest.php
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Logging;
|
||||
|
||||
use Icefox\DTO\Log;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\NullLogger;
|
||||
use Tests\Classes\PrimitiveData;
|
||||
use Tests\TestCase;
|
||||
|
||||
describe('logger resolution', function () {
|
||||
|
||||
afterEach(function () {
|
||||
config()->set('dto.log.logger', NullLogger::class);
|
||||
});
|
||||
|
||||
it('uses NullLogger as fallback when logger config is null', function () {
|
||||
config()->set('dto.log.logger', null);
|
||||
$log = new Log();
|
||||
expect($log->logger)->toBeInstanceOf(NullLogger::class);
|
||||
});
|
||||
|
||||
it('uses NullLogger as fallback when logger config is invalid', function () {
|
||||
config()->set('dto.log.logger', 'NonExistentLoggerClass');
|
||||
$log = new Log();
|
||||
expect($log->logger)->toBeInstanceOf(NullLogger::class);
|
||||
});
|
||||
|
||||
it('instantiates logger from class name via Laravel container', function () {
|
||||
config()->set('dto.log.logger', CustomLogger::class);
|
||||
$log = new Log();
|
||||
expect($log->logger)->toBeInstanceOf(CustomLogger::class);
|
||||
});
|
||||
|
||||
it('uses logger object directly when provided', function () {
|
||||
$customLogger = new CustomLogger();
|
||||
config()->set('dto.log.logger', $customLogger);
|
||||
$log = new Log();
|
||||
expect($log->logger)->toBe($customLogger);
|
||||
});
|
||||
|
||||
it('invokes callable to get logger instance', function () {
|
||||
config()->set('dto.log.logger', function () {
|
||||
return new CustomLogger();
|
||||
});
|
||||
|
||||
$log = new Log();
|
||||
|
||||
expect($log->logger)->toBeInstanceOf(CustomLogger::class);
|
||||
});
|
||||
});
|
||||
|
||||
describe('log level configuration', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->customLogger = new CustomLogger();
|
||||
config()->set('dto.log.logger', $this->customLogger);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
config()->set('dto.log.logger', NullLogger::class);
|
||||
config()->set('dto.log.rules', LogLevel::DEBUG);
|
||||
config()->set('dto.log.input', LogLevel::DEBUG);
|
||||
config()->set('dto.log.raw_input', LogLevel::DEBUG);
|
||||
config()->set('dto.log.validation_errors', LogLevel::INFO);
|
||||
});
|
||||
|
||||
it('logs rules at configured level', function () {
|
||||
config()->set('dto.log.rules', LogLevel::INFO);
|
||||
|
||||
$log = new Log();
|
||||
$log->rules(['field' => ['required']]);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::INFO, 'field'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs input at configured level', function () {
|
||||
config()->set('dto.log.input', LogLevel::INFO);
|
||||
|
||||
$log = new Log();
|
||||
$log->input(['field' => 'value']);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::INFO, 'value'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs raw input at configured level', function () {
|
||||
config()->set('dto.log.raw_input', LogLevel::ERROR);
|
||||
|
||||
$log = new Log();
|
||||
$log->inputRaw(['field' => 'raw_value']);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::ERROR, 'raw_value'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs validation errors at configured level', function () {
|
||||
config()->set('dto.log.validation_errors', LogLevel::ERROR);
|
||||
|
||||
$log = new Log();
|
||||
$log->validationErrors(['field' => ['The field is required.']]);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::ERROR, 'required'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('allows different log levels for each log type', function () {
|
||||
config()->set('dto.log.rules', LogLevel::DEBUG);
|
||||
config()->set('dto.log.input', LogLevel::INFO);
|
||||
config()->set('dto.log.raw_input', LogLevel::INFO);
|
||||
|
||||
$log = new Log();
|
||||
|
||||
$log->rules(['rules_field' => ['required']]);
|
||||
$log->input(['input_field' => 'value']);
|
||||
$log->inputRaw(['raw_field' => 'raw_value']);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'rules_field'))->toBeTrue();
|
||||
expect($this->customLogger->hasLog(LogLevel::INFO, 'input_field'))->toBeTrue();
|
||||
expect($this->customLogger->hasLog(LogLevel::INFO, 'raw_field'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('defaults to DEBUG level when not configured', function () {
|
||||
config()->set('dto.log.rules', null);
|
||||
config()->set('dto.log.input', null);
|
||||
config()->set('dto.log.raw_input', null);
|
||||
|
||||
$customLogger = new CustomLogger();
|
||||
config()->set('dto.log.logger', $customLogger);
|
||||
|
||||
$log = new Log();
|
||||
|
||||
$log->rules(['field' => ['required']]);
|
||||
$log->input(['field' => 'value']);
|
||||
$log->inputRaw(['field' => 'raw_value']);
|
||||
|
||||
expect(count($customLogger->logs))->toBe(3);
|
||||
expect($customLogger->logs[0]['level'])->toBe(LogLevel::DEBUG);
|
||||
expect($customLogger->logs[1]['level'])->toBe(LogLevel::DEBUG);
|
||||
expect($customLogger->logs[2]['level'])->toBe(LogLevel::DEBUG);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration with DataObject', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$this->customLogger = new CustomLogger();
|
||||
config()->set('dto.log.logger', $this->customLogger);
|
||||
config()->set('dto.log.rules', LogLevel::DEBUG);
|
||||
config()->set('dto.log.input', LogLevel::DEBUG);
|
||||
config()->set('dto.log.raw_input', LogLevel::DEBUG);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
config()->set('dto.log.logger', NullLogger::class);
|
||||
});
|
||||
|
||||
it('logs raw input during fromArray execution', function () {
|
||||
PrimitiveData::fromArray([
|
||||
'string' => 'test',
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'raw_input'))->toBeFalse();
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'string'))->toBeTrue();
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, '42'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs rules during fromArray execution', function () {
|
||||
PrimitiveData::fromArray([
|
||||
'string' => 'test',
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'required'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs processed input during fromArray execution', function () {
|
||||
PrimitiveData::fromArray([
|
||||
'string' => 'test',
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'test'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('captures all three log types during successful fromArray', function () {
|
||||
PrimitiveData::fromArray([
|
||||
'string' => 'integration_test',
|
||||
'int' => 123,
|
||||
'float' => 9.99,
|
||||
'bool' => false,
|
||||
]);
|
||||
|
||||
$rawInputLogged = false;
|
||||
$rulesLogged = false;
|
||||
$inputLogged = false;
|
||||
|
||||
foreach ($this->customLogger->logs as $log) {
|
||||
if (str_contains($log['message'], 'string')) {
|
||||
$rawInputLogged = true;
|
||||
}
|
||||
if (str_contains($log['message'], 'required')) {
|
||||
$rulesLogged = true;
|
||||
}
|
||||
if (str_contains($log['message'], 'integration_test')) {
|
||||
$inputLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
expect($rawInputLogged)->toBeTrue('Raw input should be logged');
|
||||
expect($rulesLogged)->toBeTrue('Rules should be logged');
|
||||
expect($inputLogged)->toBeTrue('Processed input should be logged');
|
||||
});
|
||||
|
||||
it('logs even when validation fails', function () {
|
||||
try {
|
||||
PrimitiveData::fromArray([
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, 'required'))->toBeTrue();
|
||||
expect($this->customLogger->hasLog(LogLevel::DEBUG, '42'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('logs validation errors when validation fails', function () {
|
||||
config()->set('dto.log.validation_errors', LogLevel::ERROR);
|
||||
|
||||
try {
|
||||
PrimitiveData::fromArray([
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
expect($this->customLogger->hasLog(LogLevel::ERROR, 'string'))->toBeTrue();
|
||||
expect($this->customLogger->hasLog(LogLevel::ERROR, 'required'))->toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logging with NullLogger', function () {
|
||||
|
||||
it('does not throw when logging with NullLogger', function () {
|
||||
config()->set('dto.log.logger', NullLogger::class);
|
||||
|
||||
$log = new Log();
|
||||
|
||||
expect(function () use ($log) {
|
||||
$log->rules(['field' => ['required']]);
|
||||
$log->input(['field' => 'value']);
|
||||
$log->inputRaw(['field' => 'raw_value']);
|
||||
})->not->toThrow(\Throwable::class);
|
||||
});
|
||||
|
||||
it('does not affect DataObject behavior when using NullLogger', function () {
|
||||
config()->set('dto.log.logger', NullLogger::class);
|
||||
|
||||
$object = PrimitiveData::fromArray([
|
||||
'string' => 'test',
|
||||
'int' => 42,
|
||||
'float' => 3.14,
|
||||
'bool' => true,
|
||||
]);
|
||||
|
||||
expect($object)->toBeInstanceOf(PrimitiveData::class);
|
||||
expect($object->string)->toBe('test');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue