mirror of
https://github.com/ivuorinen/monolog-gdpr-filter.git
synced 2026-03-14 10:01:41 +00:00
feat: add advanced architecture, documentation, and coverage improvements (#65)
* fix(style): resolve PHPCS line-length warnings in source files * fix(style): resolve PHPCS line-length warnings in test files * feat(audit): add structured audit logging with ErrorContext and AuditContext - ErrorContext: standardized error information with sensitive data sanitization - AuditContext: structured context for audit entries with operation types - StructuredAuditLogger: enhanced audit logger wrapper with timing support * feat(recovery): add recovery mechanism for failed masking operations - FailureMode enum: FAIL_OPEN, FAIL_CLOSED, FAIL_SAFE modes - RecoveryStrategy interface and RecoveryResult value object - RetryStrategy: exponential backoff with configurable attempts - FallbackMaskStrategy: type-aware fallback values * feat(strategies): add CallbackMaskingStrategy for custom masking logic - Wraps custom callbacks as MaskingStrategy implementations - Factory methods: constant(), hash(), partial() for common use cases - Supports exact match and prefix match for field paths * docs: add framework integration guides and examples - symfony-integration.md: Symfony service configuration and Monolog setup - psr3-decorator.md: PSR-3 logger decorator pattern implementation - framework-examples.md: CakePHP, CodeIgniter 4, Laminas, Yii2, PSR-15 - docker-development.md: Docker development environment guide * chore(docker): add Docker development environment - Dockerfile: PHP 8.2-cli-alpine with Xdebug for coverage - docker-compose.yml: development services with volume mounts * feat(demo): add interactive GDPR pattern tester playground - PatternTester.php: pattern testing utility with strategy support - index.php: web API endpoint with JSON response handling - playground.html: interactive web interface for testing patterns * docs(todo): update with completed medium priority items - Mark all PHPCS warnings as fixed (81 → 0) - Document new Audit and Recovery features - Update test count to 1,068 tests with 2,953 assertions - Move remaining items to low priority * feat: add advanced architecture, documentation, and coverage improvements - Add architecture improvements: - ArrayAccessorInterface and DotArrayAccessor for decoupled array access - MaskingOrchestrator for single-responsibility masking coordination - GdprProcessorBuilder for fluent configuration - MaskingPluginInterface and AbstractMaskingPlugin for plugin architecture - PluginAwareProcessor for plugin hook execution - AuditLoggerFactory for instance-based audit logger creation - Add advanced features: - SerializedDataProcessor for handling print_r/var_export/serialize output - KAnonymizer with GeneralizationStrategy for GDPR k-anonymity - RetentionPolicy for configurable data retention periods - StreamingProcessor for memory-efficient large log processing - Add comprehensive documentation: - docs/performance-tuning.md - benchmarking, optimization, caching - docs/troubleshooting.md - common issues and solutions - docs/logging-integrations.md - ELK, Graylog, Datadog, etc. - docs/plugin-development.md - complete plugin development guide - Improve test coverage (84.41% → 85.07%): - ConditionalRuleFactoryInstanceTest (100% coverage) - GdprProcessorBuilderEdgeCasesTest (100% coverage) - StrategyEdgeCasesTest for ReDoS detection and type parsing - 78 new tests, 119 new assertions - Update TODO.md with current statistics: - 141 PHP files, 1,346 tests, 85.07% line coverage * chore: tests, update actions, sonarcloud issues * chore: rector * fix: more sonarcloud fixes * chore: more fixes * refactor: copilot review fix * chore: rector
This commit is contained in:
118
src/Builder/GdprProcessorBuilder.php
Normal file
118
src/Builder/GdprProcessorBuilder.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\ArrayAccessor\ArrayAccessorFactory;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\Traits\CallbackConfigurationTrait;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\Traits\FieldPathConfigurationTrait;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\Traits\PatternConfigurationTrait;
|
||||
use Ivuorinen\MonologGdprFilter\Builder\Traits\PluginConfigurationTrait;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
|
||||
/**
|
||||
* Fluent builder for GdprProcessor configuration.
|
||||
*
|
||||
* Provides a clean, chainable API for configuring GdprProcessor instances
|
||||
* with support for plugins, patterns, field paths, and callbacks.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class GdprProcessorBuilder
|
||||
{
|
||||
use PatternConfigurationTrait;
|
||||
use FieldPathConfigurationTrait;
|
||||
use CallbackConfigurationTrait;
|
||||
use PluginConfigurationTrait;
|
||||
|
||||
/**
|
||||
* @var callable(string,mixed,mixed):void|null
|
||||
*/
|
||||
private $auditLogger = null;
|
||||
|
||||
private int $maxDepth = 100;
|
||||
|
||||
private ?ArrayAccessorFactory $arrayAccessorFactory = null;
|
||||
|
||||
/**
|
||||
* Create a new builder instance.
|
||||
*/
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audit logger.
|
||||
*
|
||||
* @param callable(string,mixed,mixed):void $auditLogger Audit logger callback
|
||||
*/
|
||||
public function withAuditLogger(callable $auditLogger): self
|
||||
{
|
||||
$this->auditLogger = $auditLogger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum recursion depth.
|
||||
*/
|
||||
public function withMaxDepth(int $maxDepth): self
|
||||
{
|
||||
$this->maxDepth = $maxDepth;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the array accessor factory.
|
||||
*/
|
||||
public function withArrayAccessorFactory(ArrayAccessorFactory $factory): self
|
||||
{
|
||||
$this->arrayAccessorFactory = $factory;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the GdprProcessor with all configured options.
|
||||
*
|
||||
* @throws \InvalidArgumentException When configuration is invalid
|
||||
*/
|
||||
public function build(): GdprProcessor
|
||||
{
|
||||
// Apply plugin configurations
|
||||
$this->applyPluginConfigurations();
|
||||
|
||||
return new GdprProcessor(
|
||||
$this->patterns,
|
||||
$this->fieldPaths,
|
||||
$this->customCallbacks,
|
||||
$this->auditLogger,
|
||||
$this->maxDepth,
|
||||
$this->dataTypeMasks,
|
||||
$this->conditionalRules,
|
||||
$this->arrayAccessorFactory
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a GdprProcessor wrapped with plugin hooks.
|
||||
*
|
||||
* Returns a PluginAwareProcessor if plugins are registered,
|
||||
* otherwise returns a standard GdprProcessor.
|
||||
*
|
||||
* @throws \InvalidArgumentException When configuration is invalid
|
||||
*/
|
||||
public function buildWithPlugins(): GdprProcessor|PluginAwareProcessor
|
||||
{
|
||||
$processor = $this->build();
|
||||
|
||||
if ($this->plugins === []) {
|
||||
return $processor;
|
||||
}
|
||||
|
||||
// Sort plugins by priority
|
||||
usort($this->plugins, fn($a, $b): int => $a->getPriority() <=> $b->getPriority());
|
||||
|
||||
return new PluginAwareProcessor($processor, $this->plugins);
|
||||
}
|
||||
}
|
||||
121
src/Builder/PluginAwareProcessor.php
Normal file
121
src/Builder/PluginAwareProcessor.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Contracts\MaskingPluginInterface;
|
||||
use Ivuorinen\MonologGdprFilter\GdprProcessor;
|
||||
use Monolog\LogRecord;
|
||||
use Monolog\Processor\ProcessorInterface;
|
||||
|
||||
/**
|
||||
* Wrapper that adds plugin hook support to GdprProcessor.
|
||||
*
|
||||
* Executes plugin pre/post processing hooks around the standard
|
||||
* GdprProcessor masking operations.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class PluginAwareProcessor implements ProcessorInterface
|
||||
{
|
||||
/**
|
||||
* @param GdprProcessor $processor The underlying processor
|
||||
* @param list<MaskingPluginInterface> $plugins Registered plugins (sorted by priority)
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly GdprProcessor $processor,
|
||||
private readonly array $plugins
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a log record with plugin hooks.
|
||||
*
|
||||
* @param LogRecord $record The log record to process
|
||||
* @return LogRecord The processed log record
|
||||
*/
|
||||
#[\Override]
|
||||
public function __invoke(LogRecord $record): LogRecord
|
||||
{
|
||||
// Pre-process message through plugins
|
||||
$message = $record->message;
|
||||
foreach ($this->plugins as $plugin) {
|
||||
$message = $plugin->preProcessMessage($message);
|
||||
}
|
||||
|
||||
// Pre-process context through plugins
|
||||
$context = $record->context;
|
||||
foreach ($this->plugins as $plugin) {
|
||||
$context = $plugin->preProcessContext($context);
|
||||
}
|
||||
|
||||
// Create modified record for main processor
|
||||
$modifiedRecord = $record->with(message: $message, context: $context);
|
||||
|
||||
// Apply main processor
|
||||
$processedRecord = ($this->processor)($modifiedRecord);
|
||||
|
||||
// Post-process message through plugins (reverse order)
|
||||
$message = $processedRecord->message;
|
||||
foreach (array_reverse($this->plugins) as $plugin) {
|
||||
$message = $plugin->postProcessMessage($message);
|
||||
}
|
||||
|
||||
// Post-process context through plugins (reverse order)
|
||||
$context = $processedRecord->context;
|
||||
foreach (array_reverse($this->plugins) as $plugin) {
|
||||
$context = $plugin->postProcessContext($context);
|
||||
}
|
||||
|
||||
return $processedRecord->with(message: $message, context: $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying GdprProcessor.
|
||||
*/
|
||||
public function getProcessor(): GdprProcessor
|
||||
{
|
||||
return $this->processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered plugins.
|
||||
*
|
||||
* @return list<MaskingPluginInterface>
|
||||
*/
|
||||
public function getPlugins(): array
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate regExpMessage to underlying processor.
|
||||
*/
|
||||
public function regExpMessage(string $message = ''): string
|
||||
{
|
||||
return $this->processor->regExpMessage($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate recursiveMask to underlying processor.
|
||||
*
|
||||
* @param array<mixed>|string $data
|
||||
* @param int $currentDepth
|
||||
* @return array<mixed>|string
|
||||
*/
|
||||
public function recursiveMask(array|string $data, int $currentDepth = 0): array|string
|
||||
{
|
||||
return $this->processor->recursiveMask($data, $currentDepth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate setAuditLogger to underlying processor.
|
||||
*
|
||||
* @param callable(string,mixed,mixed):void|null $auditLogger
|
||||
*/
|
||||
public function setAuditLogger(?callable $auditLogger): void
|
||||
{
|
||||
$this->processor->setAuditLogger($auditLogger);
|
||||
}
|
||||
}
|
||||
100
src/Builder/Traits/CallbackConfigurationTrait.php
Normal file
100
src/Builder/Traits/CallbackConfigurationTrait.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder\Traits;
|
||||
|
||||
use Monolog\LogRecord;
|
||||
|
||||
/**
|
||||
* Provides callback configuration methods for GdprProcessorBuilder.
|
||||
*
|
||||
* Handles custom callbacks, data type masks, and conditional masking rules
|
||||
* for advanced masking scenarios.
|
||||
*/
|
||||
trait CallbackConfigurationTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string,callable(mixed):string>
|
||||
*/
|
||||
private array $customCallbacks = [];
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private array $dataTypeMasks = [];
|
||||
|
||||
/**
|
||||
* @var array<string,callable(LogRecord):bool>
|
||||
*/
|
||||
private array $conditionalRules = [];
|
||||
|
||||
/**
|
||||
* Add a custom callback for a field path.
|
||||
*
|
||||
* @param string $path Dot-notation path
|
||||
* @param callable(mixed):string $callback Transformation callback
|
||||
*/
|
||||
public function addCallback(string $path, callable $callback): self
|
||||
{
|
||||
$this->customCallbacks[$path] = $callback;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple custom callbacks.
|
||||
*
|
||||
* @param array<string,callable(mixed):string> $callbacks Path => callback
|
||||
*/
|
||||
public function addCallbacks(array $callbacks): self
|
||||
{
|
||||
$this->customCallbacks = array_merge($this->customCallbacks, $callbacks);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a data type mask.
|
||||
*
|
||||
* @param string $type Data type (e.g., 'integer', 'double', 'boolean')
|
||||
* @param string $mask Replacement mask
|
||||
*/
|
||||
public function addDataTypeMask(string $type, string $mask): self
|
||||
{
|
||||
$this->dataTypeMasks[$type] = $mask;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple data type masks.
|
||||
*
|
||||
* @param array<string,string> $masks Type => mask
|
||||
*/
|
||||
public function addDataTypeMasks(array $masks): self
|
||||
{
|
||||
$this->dataTypeMasks = array_merge($this->dataTypeMasks, $masks);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a conditional masking rule.
|
||||
*
|
||||
* @param string $name Rule name
|
||||
* @param callable(LogRecord):bool $condition Condition callback
|
||||
*/
|
||||
public function addConditionalRule(string $name, callable $condition): self
|
||||
{
|
||||
$this->conditionalRules[$name] = $condition;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple conditional rules.
|
||||
*
|
||||
* @param array<string,callable(LogRecord):bool> $rules Name => condition
|
||||
*/
|
||||
public function addConditionalRules(array $rules): self
|
||||
{
|
||||
$this->conditionalRules = array_merge($this->conditionalRules, $rules);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
65
src/Builder/Traits/FieldPathConfigurationTrait.php
Normal file
65
src/Builder/Traits/FieldPathConfigurationTrait.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder\Traits;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\FieldMaskConfig;
|
||||
|
||||
/**
|
||||
* Provides field path configuration methods for GdprProcessorBuilder.
|
||||
*
|
||||
* Handles field path management for masking specific fields in log context
|
||||
* using dot notation (e.g., "user.email").
|
||||
*/
|
||||
trait FieldPathConfigurationTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string,FieldMaskConfig|string>
|
||||
*/
|
||||
private array $fieldPaths = [];
|
||||
|
||||
/**
|
||||
* Add a field path to mask.
|
||||
*
|
||||
* @param string $path Dot-notation path
|
||||
* @param FieldMaskConfig|string $config Mask configuration or replacement string
|
||||
*/
|
||||
public function addFieldPath(string $path, FieldMaskConfig|string $config): self
|
||||
{
|
||||
$this->fieldPaths[$path] = $config;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple field paths.
|
||||
*
|
||||
* @param array<string,FieldMaskConfig|string> $fieldPaths Path => config
|
||||
*/
|
||||
public function addFieldPaths(array $fieldPaths): self
|
||||
{
|
||||
$this->fieldPaths = array_merge($this->fieldPaths, $fieldPaths);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all field paths (replaces existing).
|
||||
*
|
||||
* @param array<string,FieldMaskConfig|string> $fieldPaths Path => config
|
||||
*/
|
||||
public function setFieldPaths(array $fieldPaths): self
|
||||
{
|
||||
$this->fieldPaths = $fieldPaths;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current field paths configuration.
|
||||
*
|
||||
* @return array<string,FieldMaskConfig|string>
|
||||
*/
|
||||
public function getFieldPaths(): array
|
||||
{
|
||||
return $this->fieldPaths;
|
||||
}
|
||||
}
|
||||
74
src/Builder/Traits/PatternConfigurationTrait.php
Normal file
74
src/Builder/Traits/PatternConfigurationTrait.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder\Traits;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\DefaultPatterns;
|
||||
|
||||
/**
|
||||
* Provides pattern configuration methods for GdprProcessorBuilder.
|
||||
*
|
||||
* Handles regex pattern management including adding, setting, and retrieving patterns
|
||||
* used for masking sensitive data in log records.
|
||||
*/
|
||||
trait PatternConfigurationTrait
|
||||
{
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private array $patterns = [];
|
||||
|
||||
/**
|
||||
* Add a regex pattern.
|
||||
*
|
||||
* @param string $pattern Regex pattern
|
||||
* @param string $replacement Replacement string
|
||||
*/
|
||||
public function addPattern(string $pattern, string $replacement): self
|
||||
{
|
||||
$this->patterns[$pattern] = $replacement;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple patterns.
|
||||
*
|
||||
* @param array<string,string> $patterns Regex pattern => replacement
|
||||
*/
|
||||
public function addPatterns(array $patterns): self
|
||||
{
|
||||
$this->patterns = array_merge($this->patterns, $patterns);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all patterns (replaces existing).
|
||||
*
|
||||
* @param array<string,string> $patterns Regex pattern => replacement
|
||||
*/
|
||||
public function setPatterns(array $patterns): self
|
||||
{
|
||||
$this->patterns = $patterns;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current patterns configuration.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public function getPatterns(): array
|
||||
{
|
||||
return $this->patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start with default GDPR patterns.
|
||||
*/
|
||||
public function withDefaultPatterns(): self
|
||||
{
|
||||
$this->patterns = array_merge($this->patterns, DefaultPatterns::get());
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
67
src/Builder/Traits/PluginConfigurationTrait.php
Normal file
67
src/Builder/Traits/PluginConfigurationTrait.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Ivuorinen\MonologGdprFilter\Builder\Traits;
|
||||
|
||||
use Ivuorinen\MonologGdprFilter\Contracts\MaskingPluginInterface;
|
||||
|
||||
/**
|
||||
* Provides plugin configuration methods for GdprProcessorBuilder.
|
||||
*
|
||||
* Handles registration and management of masking plugins that can extend
|
||||
* the processor's functionality with custom patterns and field paths.
|
||||
*/
|
||||
trait PluginConfigurationTrait
|
||||
{
|
||||
/**
|
||||
* @var list<MaskingPluginInterface>
|
||||
*/
|
||||
private array $plugins = [];
|
||||
|
||||
/**
|
||||
* Register a masking plugin.
|
||||
*/
|
||||
public function addPlugin(MaskingPluginInterface $plugin): self
|
||||
{
|
||||
$this->plugins[] = $plugin;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register multiple masking plugins.
|
||||
*
|
||||
* @param list<MaskingPluginInterface> $plugins
|
||||
*/
|
||||
public function addPlugins(array $plugins): self
|
||||
{
|
||||
foreach ($plugins as $plugin) {
|
||||
$this->plugins[] = $plugin;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get registered plugins.
|
||||
*
|
||||
* @return list<MaskingPluginInterface>
|
||||
*/
|
||||
public function getPlugins(): array
|
||||
{
|
||||
return $this->plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply plugin patterns and field paths to the builder configuration.
|
||||
*/
|
||||
private function applyPluginConfigurations(): void
|
||||
{
|
||||
// Sort plugins by priority before applying
|
||||
usort($this->plugins, fn($a, $b): int => $a->getPriority() <=> $b->getPriority());
|
||||
|
||||
foreach ($this->plugins as $plugin) {
|
||||
$this->patterns = array_merge($this->patterns, $plugin->getPatterns());
|
||||
$this->fieldPaths = array_merge($this->fieldPaths, $plugin->getFieldPaths());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user