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:
2025-12-22 13:38:18 +02:00
committed by GitHub
parent b1eb567b92
commit 8866daaf33
112 changed files with 15391 additions and 607 deletions

View 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);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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());
}
}
}