feat: performance, integrations, advanced features (#2)

* feat: performance, integrations, advanced features

* chore: fix linting problems

* chore: suppressions and linting

* chore(lint): pre-commit linting, fixes

* feat: comprehensive input validation, security hardening, and regression testing

- Add extensive input validation throughout codebase with proper error handling
- Implement comprehensive security hardening with ReDoS protection and bounds checking
- Add 3 new regression test suites covering critical bugs, security, and validation scenarios
- Enhance rate limiting with memory management and configurable cleanup intervals
- Update configuration security settings and improve Laravel integration
- Fix TODO.md timestamps to reflect actual development timeline
- Strengthen static analysis configuration and improve code quality standards

* feat: configure static analysis tools and enhance development workflow

- Complete configuration of Psalm, PHPStan, and Rector for harmonious static analysis.
- Fix invalid configurations and tool conflicts that prevented proper code quality analysis.
- Add comprehensive safe analysis script with interactive workflow, backup/restore
  capabilities, and dry-run modes. Update documentation with linting policy
  requiring issue resolution over suppression.
- Clean completed items from TODO to focus on actionable improvements.
- All static analysis tools now work together seamlessly to provide
  code quality insights without breaking existing functionality.

* fix(test): update Invalid regex pattern expectation

* chore: phpstan, psalm fixes

* chore: phpstan, psalm fixes, more tests

* chore: tooling tweaks, cleanup

* chore: tweaks to get the tests pass

* fix(lint): rector config tweaks and successful run

* feat: refactoring, more tests, fixes, cleanup

* chore: deduplication, use constants

* chore: psalm fixes

* chore: ignore phpstan deliberate errors in tests

* chore: improve codebase, deduplicate code

* fix: lint

* chore: deduplication, codebase simplification, sonarqube fixes

* fix: resolve SonarQube reliability rating issues

Fix useless object instantiation warnings in test files by assigning
instantiated objects to variables. This resolves the SonarQube reliability
rating issue (was C, now targeting A).

Changes:
- tests/Strategies/MaskingStrategiesTest.php: Fix 3 instances
- tests/Strategies/FieldPathMaskingStrategyTest.php: Fix 1 instance

The tests use expectException() to verify that constructors throw
exceptions for invalid input. SonarQube flagged standalone `new`
statements as useless. Fixed by assigning to variables with explicit
unset() and fail() calls.

All tests pass (623/623) and static analysis tools pass.

* fix: resolve more SonarQube detected issues

* fix: resolve psalm detected issues

* fix: resolve more SonarQube detected issues

* fix: resolve psalm detected issues

* fix: duplications

* fix: resolve SonarQube reliability rating issues

* fix: resolve psalm and phpstan detected issues
This commit is contained in:
2025-10-31 13:59:01 +02:00
committed by GitHub
parent 63637900c8
commit 00c6f76c97
126 changed files with 30815 additions and 921 deletions

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when audit logging operations fail.
*
* This exception is thrown when:
* - An audit logger callback throws an exception
* - Audit log data cannot be serialized
* - Rate-limited audit logging encounters errors
* - Audit logger configuration is invalid
*
* @api
*/
class AuditLoggingException extends GdprProcessorException
{
/**
* Create an exception for a failed audit logging callback.
*
* @param string $path The field path being audited
* @param mixed $original The original value
* @param mixed $masked The masked value
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function callbackFailed(
string $path,
mixed $original,
mixed $masked,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Audit logging callback failed for path '%s': %s", $path, $reason);
return self::withContext($message, [
'audit_type' => 'callback_failure',
'path' => $path,
'original_type' => gettype($original),
'masked_type' => gettype($masked),
'original_preview' => self::getValuePreview($original),
'masked_preview' => self::getValuePreview($masked),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for audit data serialization failure.
*
* @param string $path The field path being audited
* @param mixed $value The value that failed to serialize
* @param string $reason The reason for the serialization failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function serializationFailed(
string $path,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Audit data serialization failed for path '%s': %s", $path, $reason);
return self::withContext($message, [
'audit_type' => 'serialization_failure',
'path' => $path,
'value_type' => gettype($value),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for rate-limited audit logging failures.
*
* @param string $operationType The operation type being rate limited
* @param int $currentRequests Current number of requests
* @param int $maxRequests Maximum allowed requests
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function rateLimitingFailed(
string $operationType,
int $currentRequests,
int $maxRequests,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Rate-limited audit logging failed for operation '%s': %s", $operationType, $reason);
return self::withContext($message, [
'audit_type' => 'rate_limiting_failure',
'operation_type' => $operationType,
'current_requests' => $currentRequests,
'max_requests' => $maxRequests,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for invalid audit logger configuration.
*
* @param string $configurationIssue Description of the configuration issue
* @param array<string, mixed> $config The invalid configuration
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidConfiguration(
string $configurationIssue,
array $config,
?Throwable $previous = null
): static {
$message = 'Invalid audit logger configuration: ' . $configurationIssue;
return self::withContext($message, [
'audit_type' => 'configuration_error',
'configuration_issue' => $configurationIssue,
'config' => $config,
], 0, $previous);
}
/**
* Create an exception for audit logger creation failure.
*
* @param string $loggerType The type of logger being created
* @param string $reason The reason for the creation failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function loggerCreationFailed(
string $loggerType,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Audit logger creation failed for type '%s': %s", $loggerType, $reason);
return self::withContext($message, [
'audit_type' => 'logger_creation_failure',
'logger_type' => $loggerType,
'reason' => $reason,
], 0, $previous);
}
/**
* Get a safe preview of a value for logging.
*
* @param mixed $value The value to preview
* @return string Safe preview string
*/
private static function getValuePreview(mixed $value): string
{
if (is_string($value)) {
return substr($value, 0, 100) . (strlen($value) > 100 ? '...' : '');
}
if (is_array($value) || is_object($value)) {
$json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
if ($json === false) {
return '[Unable to serialize]';
}
return substr($json, 0, 100) . (strlen($json) > 100 ? '...' : '');
}
return (string) $value;
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when command execution fails.
*
* This exception is thrown when:
* - Artisan commands encounter runtime errors
* - Command input validation fails
* - Command operations fail during execution
* - Command result processing fails
* - File operations within commands fail
*
* @api
*/
class CommandExecutionException extends GdprProcessorException
{
/**
* Create an exception for command input validation failure.
*
* @param string $commandName The command that failed
* @param string $inputName The input parameter that failed validation
* @param mixed $inputValue The invalid input value
* @param string $reason The reason for validation failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forInvalidInput(
string $commandName,
string $inputName,
mixed $inputValue,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf(
"Command '%s' failed: invalid input '%s' - %s",
$commandName,
$inputName,
$reason
);
return self::withContext($message, [
'command_name' => $commandName,
'input_name' => $inputName,
'input_value' => $inputValue,
'reason' => $reason,
'category' => 'input_validation',
], 0, $previous);
}
/**
* Create an exception for command operation failure.
*
* @param string $commandName The command that failed
* @param string $operation The operation that failed
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forOperation(
string $commandName,
string $operation,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf(
"Command '%s' failed during operation '%s': %s",
$commandName,
$operation,
$reason
);
return self::withContext($message, [
'command_name' => $commandName,
'operation' => $operation,
'reason' => $reason,
'category' => 'operation_failure',
], 0, $previous);
}
/**
* Create an exception for pattern testing failure.
*
* @param string $pattern The pattern that failed testing
* @param string $testString The test string used
* @param string $reason The reason for test failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forPatternTest(
string $pattern,
string $testString,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Pattern test failed for '%s': %s", $pattern, $reason);
return self::withContext($message, [
'pattern' => $pattern,
'test_string' => $testString,
'reason' => $reason,
'category' => 'pattern_test',
], 0, $previous);
}
/**
* Create an exception for JSON processing failure in commands.
*
* @param string $commandName The command that failed
* @param string $jsonData The JSON data being processed
* @param string $reason The reason for JSON processing failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forJsonProcessing(
string $commandName,
string $jsonData,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf(
"Command '%s' failed to process JSON data: %s",
$commandName,
$reason
);
return self::withContext($message, [
'command_name' => $commandName,
'json_data' => $jsonData,
'reason' => $reason,
'category' => 'json_processing',
], 0, $previous);
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
use Exception;
/**
* Base exception class for all GDPR processor related errors.
*
* This serves as the parent class for all specific GDPR processing exceptions,
* allowing consumers to catch all GDPR-related errors with a single catch block.
*
* @api
*/
class GdprProcessorException extends Exception
{
/**
* Create a new GDPR processor exception.
*
* @param string $message The exception message
* @param int $code The exception code (default: 0)
* @param Throwable|null $previous Previous exception for chaining
*/
public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
/**
* Create an exception with additional context information.
*
* @param string $message The base exception message
* @param array<string, mixed> $context Additional context data
* @param int $code The exception code (default: 0)
* @param Throwable|null $previous Previous exception for chaining
*/
public static function withContext(
string $message,
array $context,
int $code = 0,
?Throwable $previous = null
): static {
$contextString = '';
if ($context !== []) {
$contextParts = [];
foreach ($context as $key => $value) {
$encoded = json_encode($value, JSON_UNESCAPED_SLASHES);
$contextParts[] = $key . ': ' . ($encoded === false ? '[unserializable]' : $encoded);
}
$contextString = ' [Context: ' . implode(', ', $contextParts) . ']';
}
/**
* @psalm-suppress UnsafeInstantiation
* @phpstan-ignore new.static
*/
return new static($message . $contextString, $code, $previous);
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when GDPR processor configuration is invalid.
*
* This exception is thrown when:
* - Invalid field paths are provided
* - Invalid data type masks are specified
* - Invalid conditional rules are configured
* - Configuration values are out of acceptable ranges
* - Configuration structure is malformed
*
* @api
*/
class InvalidConfigurationException extends GdprProcessorException
{
/**
* Create an exception for an invalid field path.
*
* @param string $fieldPath The invalid field path
* @param string $reason The reason why the field path is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forFieldPath(
string $fieldPath,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Invalid field path '%s': %s", $fieldPath, $reason);
return self::withContext($message, [
'field_path' => $fieldPath,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for an invalid data type mask.
*
* @param string $dataType The invalid data type
* @param mixed $mask The invalid mask value
* @param string $reason The reason why the mask is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forDataTypeMask(
string $dataType,
mixed $mask,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Invalid data type mask for '%s': %s", $dataType, $reason);
return self::withContext($message, [
'data_type' => $dataType,
'mask' => $mask,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for an invalid conditional rule.
*
* @param string $ruleName The invalid rule name
* @param string $reason The reason why the rule is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forConditionalRule(
string $ruleName,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Invalid conditional rule '%s': %s", $ruleName, $reason);
return self::withContext($message, [
'rule_name' => $ruleName,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for an invalid configuration value.
*
* @param string $parameter The parameter name
* @param mixed $value The invalid value
* @param string $reason The reason why the value is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forParameter(
string $parameter,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Invalid configuration parameter '%s': %s", $parameter, $reason);
return self::withContext($message, [
'parameter' => $parameter,
'value' => $value,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for an empty or null required value.
*
* @param string $parameter The parameter name that cannot be empty
* @param Throwable|null $previous Previous exception for chaining
*/
public static function emptyValue(
string $parameter,
?Throwable $previous = null
): static {
$message = sprintf("%s cannot be empty", ucfirst($parameter));
return self::withContext($message, [
'parameter' => $parameter,
], 0, $previous);
}
/**
* Create an exception for a value that exceeds maximum allowed length.
*
* @param string $parameter The parameter name
* @param int $actualLength The actual length
* @param int $maxLength The maximum allowed length
* @param Throwable|null $previous Previous exception for chaining
*/
public static function exceedsMaxLength(
string $parameter,
int $actualLength,
int $maxLength,
?Throwable $previous = null
): static {
$message = sprintf(
"%s length (%d) exceeds maximum allowed length (%d)",
ucfirst($parameter),
$actualLength,
$maxLength
);
return self::withContext($message, [
'parameter' => $parameter,
'actual_length' => $actualLength,
'max_length' => $maxLength,
], 0, $previous);
}
/**
* Create an exception for an invalid type.
*
* @param string $parameter The parameter name
* @param string $expectedType The expected type
* @param string $actualType The actual type
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidType(
string $parameter,
string $expectedType,
string $actualType,
?Throwable $previous = null
): static {
$message = sprintf(
"%s must be of type %s, got %s",
ucfirst($parameter),
$expectedType,
$actualType
);
return self::withContext($message, [
'parameter' => $parameter,
'expected_type' => $expectedType,
'actual_type' => $actualType,
], 0, $previous);
}
}

View File

@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when rate limiter configuration is invalid.
*
* This exception is thrown when:
* - Maximum requests value is invalid
* - Time window value is invalid
* - Cleanup interval value is invalid
* - Rate limiting key is invalid or contains forbidden characters
*
* @api
*/
class InvalidRateLimitConfigurationException extends GdprProcessorException
{
/**
* Create an exception for an invalid maximum requests value.
*
* @param int|float|string $value The invalid value
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidMaxRequests(
int|float|string $value,
?Throwable $previous = null
): static {
$message = sprintf('Maximum requests must be a positive integer, got: %s', $value);
return self::withContext($message, [
'parameter' => 'max_requests',
'value' => $value,
], 0, $previous);
}
/**
* Create an exception for an invalid time window value.
*
* @param int|float|string $value The invalid value
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidTimeWindow(
int|float|string $value,
?Throwable $previous = null
): static {
$message = sprintf(
'Time window must be a positive integer representing seconds, got: %s',
$value
);
return self::withContext($message, [
'parameter' => 'time_window',
'value' => $value,
], 0, $previous);
}
/**
* Create an exception for an invalid cleanup interval.
*
* @param int|float|string $value The invalid value
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidCleanupInterval(
int|float|string $value,
?Throwable $previous = null
): static {
$message = sprintf('Cleanup interval must be a positive integer, got: %s', $value);
return self::withContext($message, [
'parameter' => 'cleanup_interval',
'value' => $value,
], 0, $previous);
}
/**
* Create an exception for a time window that is too short.
*
* @param int $value The time window value
* @param int $minimum The minimum allowed value
* @param Throwable|null $previous Previous exception for chaining
*/
public static function timeWindowTooShort(
int $value,
int $minimum,
?Throwable $previous = null
): static {
$message = sprintf(
'Time window (%d seconds) is too short, minimum is %d seconds',
$value,
$minimum
);
return self::withContext($message, [
'parameter' => 'time_window',
'value' => $value,
'minimum' => $minimum,
], 0, $previous);
}
/**
* Create an exception for a cleanup interval that is too short.
*
* @param int $value The cleanup interval value
* @param int $minimum The minimum allowed value
* @param Throwable|null $previous Previous exception for chaining
*/
public static function cleanupIntervalTooShort(
int $value,
int $minimum,
?Throwable $previous = null
): static {
$message = sprintf(
'Cleanup interval (%d seconds) is too short, minimum is %d seconds',
$value,
$minimum
);
return self::withContext($message, [
'parameter' => 'cleanup_interval',
'value' => $value,
'minimum' => $minimum,
], 0, $previous);
}
/**
* Create an exception for an empty rate limiting key.
*
* @param Throwable|null $previous Previous exception for chaining
*/
public static function emptyKey(?Throwable $previous = null): static
{
return self::withContext('Rate limiting key cannot be empty', [
'parameter' => 'key',
], 0, $previous);
}
/**
* Create an exception for a rate limiting key that is too long.
*
* @param string $key The key that is too long
* @param int $maxLength The maximum allowed length
* @param Throwable|null $previous Previous exception for chaining
*/
public static function keyTooLong(
string $key,
int $maxLength,
?Throwable $previous = null
): static {
$message = sprintf(
'Rate limiting key length (%d) exceeds maximum (%d characters)',
strlen($key),
$maxLength
);
return self::withContext($message, [
'parameter' => 'key',
'key_length' => strlen($key),
'max_length' => $maxLength,
], 0, $previous);
}
/**
* Create an exception for a rate limiting key containing invalid characters.
*
* @param string $reason The reason why the key is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidKeyFormat(
string $reason,
?Throwable $previous = null
): static {
return self::withContext($reason, [
'parameter' => 'key',
], 0, $previous);
}
/**
* Create an exception for a generic parameter validation failure.
*
* @param string $parameter The parameter name
* @param mixed $value The invalid value
* @param string $reason The reason why the value is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forParameter(
string $parameter,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Invalid rate limit parameter '%s': %s", $parameter, $reason);
return self::withContext($message, [
'parameter' => $parameter,
'value' => $value,
'reason' => $reason,
], 0, $previous);
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when a regex pattern is invalid or cannot be compiled.
*
* This exception is thrown when:
* - A regex pattern has invalid syntax
* - A regex pattern cannot be compiled by PHP's PCRE engine
* - A regex pattern is detected as potentially vulnerable to ReDoS attacks
* - A regex pattern compilation results in a PCRE error
*
* @api
*/
class InvalidRegexPatternException extends GdprProcessorException
{
/**
* Create an exception for an invalid regex pattern.
*
* @param string $pattern The invalid regex pattern
* @param string $reason The reason why the pattern is invalid
* @param int $pcreError Optional PCRE error code
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forPattern(
string $pattern,
string $reason,
int $pcreError = 0,
?Throwable $previous = null
): static {
$message = sprintf("Invalid regex pattern '%s': %s", $pattern, $reason);
if ($pcreError !== 0) {
$pcreErrorMessage = self::getPcreErrorMessage($pcreError);
$message .= sprintf(' (PCRE Error: %s)', $pcreErrorMessage);
}
return self::withContext($message, [
'pattern' => $pattern,
'reason' => $reason,
'pcre_error' => $pcreError,
'pcre_error_message' => $pcreError !== 0 ? self::getPcreErrorMessage($pcreError) : null,
], $pcreError, $previous);
}
/**
* Create an exception for a pattern that failed compilation.
*
* @param string $pattern The pattern that failed to compile
* @param int $pcreError The PCRE error code
* @param Throwable|null $previous Previous exception for chaining
*/
public static function compilationFailed(
string $pattern,
int $pcreError,
?Throwable $previous = null
): static {
return self::forPattern($pattern, 'Pattern compilation failed', $pcreError, $previous);
}
/**
* Create an exception for a pattern detected as vulnerable to ReDoS.
*
* @param string $pattern The potentially vulnerable pattern
* @param string $vulnerability Description of the vulnerability
* @param Throwable|null $previous Previous exception for chaining
*
* @return InvalidRegexPatternException&static
*/
public static function redosVulnerable(
string $pattern,
string $vulnerability,
?Throwable $previous = null
): static {
return self::forPattern($pattern, 'Potential ReDoS vulnerability: ' . $vulnerability, 0, $previous);
}
/**
* Get a human-readable error message for a PCRE error code.
*
* @param int $errorCode The PCRE error code
*
* @return string Human-readable error message
* @psalm-return non-empty-string
*/
private static function getPcreErrorMessage(int $errorCode): string
{
return match ($errorCode) {
PREG_NO_ERROR => 'No error',
PREG_INTERNAL_ERROR => 'Internal PCRE error',
PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit exceeded',
PREG_RECURSION_LIMIT_ERROR => 'Recursion limit exceeded',
PREG_BAD_UTF8_ERROR => 'Invalid UTF-8 data',
PREG_BAD_UTF8_OFFSET_ERROR => 'Invalid UTF-8 offset',
PREG_JIT_STACKLIMIT_ERROR => 'JIT stack limit exceeded',
default => sprintf('Unknown PCRE error (code: %s)', $errorCode),
};
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when a masking operation fails unexpectedly.
*
* This exception is thrown when:
* - A regex replacement operation fails
* - A field path masking operation encounters an error
* - A custom callback masking function throws an exception
* - Data type masking fails due to type conversion issues
* - JSON masking fails due to malformed JSON structures
*
* @api
*/
class MaskingOperationFailedException extends GdprProcessorException
{
/**
* Create an exception for a failed regex masking operation.
*
* @param string $pattern The regex pattern that failed
* @param string $input The input string being processed
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function regexMaskingFailed(
string $pattern,
string $input,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Regex masking failed for pattern '%s': %s", $pattern, $reason);
return self::withContext($message, [
'operation_type' => 'regex_masking',
'pattern' => $pattern,
'input_length' => strlen($input),
'input_preview' => substr($input, 0, 100) . (strlen($input) > 100 ? '...' : ''),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for a failed field path masking operation.
*
* @param string $fieldPath The field path that failed
* @param mixed $value The value being masked
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function fieldPathMaskingFailed(
string $fieldPath,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Field path masking failed for path '%s': %s", $fieldPath, $reason);
return self::withContext($message, [
'operation_type' => 'field_path_masking',
'field_path' => $fieldPath,
'value_type' => gettype($value),
'value_preview' => self::getValuePreview($value),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for a failed custom callback masking operation.
*
* @param string $fieldPath The field path with the custom callback
* @param mixed $value The value being processed
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function customCallbackFailed(
string $fieldPath,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Custom callback masking failed for path '%s': %s", $fieldPath, $reason);
return self::withContext($message, [
'operation_type' => 'custom_callback',
'field_path' => $fieldPath,
'value_type' => gettype($value),
'value_preview' => self::getValuePreview($value),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for a failed data type masking operation.
*
* @param string $dataType The data type being masked
* @param mixed $value The value being masked
* @param string $reason The reason for the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function dataTypeMaskingFailed(
string $dataType,
mixed $value,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Data type masking failed for type '%s': %s", $dataType, $reason);
return self::withContext($message, [
'operation_type' => 'data_type_masking',
'expected_type' => $dataType,
'actual_type' => gettype($value),
'value_preview' => self::getValuePreview($value),
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for a failed JSON masking operation.
*
* @param string $jsonString The JSON string that failed to be processed
* @param string $reason The reason for the failure
* @param int $jsonError Optional JSON error code
* @param Throwable|null $previous Previous exception for chaining
*/
public static function jsonMaskingFailed(
string $jsonString,
string $reason,
int $jsonError = 0,
?Throwable $previous = null
): static {
$message = 'JSON masking failed: ' . $reason;
if ($jsonError !== 0) {
$jsonErrorMessage = json_last_error_msg();
$message .= sprintf(' (JSON Error: %s)', $jsonErrorMessage);
}
return self::withContext($message, [
'operation_type' => 'json_masking',
'json_preview' => substr($jsonString, 0, 200) . (strlen($jsonString) > 200 ? '...' : ''),
'json_length' => strlen($jsonString),
'reason' => $reason,
'json_error' => $jsonError,
'json_error_message' => $jsonError !== 0 ? json_last_error_msg() : null,
], 0, $previous);
}
/**
* Get a safe preview of a value for logging.
*
* @param mixed $value The value to preview
* @return string Safe preview string
*/
private static function getValuePreview(mixed $value): string
{
if (is_string($value)) {
return substr($value, 0, 100) . (strlen($value) > 100 ? '...' : '');
}
if (is_array($value) || is_object($value)) {
$json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
if ($json === false) {
return '[Unable to serialize]';
}
return substr($json, 0, 100) . (strlen($json) > 100 ? '...' : '');
}
return (string) $value;
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when pattern validation fails.
*
* This exception is thrown when:
* - Regex patterns are invalid or malformed
* - Pattern security validation fails
* - Pattern syntax is incorrect
* - Pattern validation methods encounter errors
*
* @api
*/
class PatternValidationException extends GdprProcessorException
{
/**
* Create an exception for a failed pattern validation.
*
* @param string $pattern The pattern that failed validation
* @param string $reason The reason why validation failed
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forPattern(
string $pattern,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Pattern validation failed for '%s': %s", $pattern, $reason);
return self::withContext($message, [
'pattern' => $pattern,
'reason' => $reason,
], 0, $previous);
}
/**
* Create an exception for multiple pattern validation failures.
*
* @param array<string, string> $failedPatterns Array of pattern => error reason
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forMultiplePatterns(
array $failedPatterns,
?Throwable $previous = null
): static {
$count = count($failedPatterns);
$message = sprintf("Pattern validation failed for %d pattern(s)", $count);
return self::withContext($message, [
'failed_patterns' => $failedPatterns,
'failure_count' => $count,
], 0, $previous);
}
/**
* Create an exception for pattern security validation failure.
*
* @param string $pattern The potentially unsafe pattern
* @param string $securityReason The security concern
* @param Throwable|null $previous Previous exception for chaining
*/
public static function securityValidationFailed(
string $pattern,
string $securityReason,
?Throwable $previous = null
): static {
$message = sprintf("Pattern security validation failed for '%s': %s", $pattern, $securityReason);
return self::withContext($message, [
'pattern' => $pattern,
'security_reason' => $securityReason,
'category' => 'security',
], 0, $previous);
}
/**
* Create an exception for pattern syntax errors.
*
* @param string $pattern The pattern with syntax errors
* @param string $syntaxError The syntax error details
* @param Throwable|null $previous Previous exception for chaining
*/
public static function syntaxError(
string $pattern,
string $syntaxError,
?Throwable $previous = null
): static {
$message = sprintf("Pattern syntax error in '%s': %s", $pattern, $syntaxError);
return self::withContext($message, [
'pattern' => $pattern,
'syntax_error' => $syntaxError,
'category' => 'syntax',
], 0, $previous);
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when the maximum recursion depth is exceeded during processing.
*
* This exception is thrown when:
* - The recursion depth limit is exceeded while processing nested structures
* - Circular references are detected in data structures
* - Extremely deep nesting threatens stack overflow
* - The configured maxDepth parameter is reached
*
* @api
*/
class RecursionDepthExceededException extends GdprProcessorException
{
/**
* Create an exception for exceeded recursion depth.
*
* @param int $currentDepth The current recursion depth when the exception occurred
* @param int $maxDepth The maximum allowed recursion depth
* @param string $path The field path where the depth was exceeded
* @param Throwable|null $previous Previous exception for chaining
*/
public static function depthExceeded(
int $currentDepth,
int $maxDepth,
string $path,
?Throwable $previous = null
): static {
$message = sprintf(
"Maximum recursion depth of %d exceeded (current: %d) at path '%s'",
$maxDepth,
$currentDepth,
$path
);
return self::withContext($message, [
'error_type' => 'depth_exceeded',
'current_depth' => $currentDepth,
'max_depth' => $maxDepth,
'field_path' => $path,
'safety_measure' => 'Processing stopped to prevent stack overflow',
], 0, $previous);
}
/**
* Create an exception for potential circular reference detection.
*
* @param string $path The field path where circular reference was detected
* @param int $currentDepth The current recursion depth
* @param int $maxDepth The maximum allowed recursion depth
* @param Throwable|null $previous Previous exception for chaining
*/
public static function circularReferenceDetected(
string $path,
int $currentDepth,
int $maxDepth,
?Throwable $previous = null
): static {
$message = sprintf(
"Potential circular reference detected at path '%s' (depth: %d/%d)",
$path,
$currentDepth,
$maxDepth
);
return self::withContext($message, [
'error_type' => 'circular_reference',
'field_path' => $path,
'current_depth' => $currentDepth,
'max_depth' => $maxDepth,
'safety_measure' => 'Processing stopped to prevent infinite recursion',
], 0, $previous);
}
/**
* Create an exception for extremely deep nesting scenarios.
*
* @param string $dataType The type of data structure causing deep nesting
* @param int $currentDepth The current recursion depth
* @param int $maxDepth The maximum allowed recursion depth
* @param string $path The field path with deep nesting
* @param Throwable|null $previous Previous exception for chaining
*/
public static function extremeNesting(
string $dataType,
int $currentDepth,
int $maxDepth,
string $path,
?Throwable $previous = null
): static {
$message = sprintf(
"Extremely deep nesting detected in %s at path '%s' (depth: %d/%d)",
$dataType,
$path,
$currentDepth,
$maxDepth
);
return self::withContext($message, [
'error_type' => 'extreme_nesting',
'data_type' => $dataType,
'field_path' => $path,
'current_depth' => $currentDepth,
'max_depth' => $maxDepth,
'suggestion' => 'Consider flattening the data structure or increasing maxDepth parameter',
], 0, $previous);
}
/**
* Create an exception for invalid depth configuration.
*
* @param int $invalidDepth The invalid depth value provided
* @param string $reason The reason why the depth is invalid
* @param Throwable|null $previous Previous exception for chaining
*/
public static function invalidDepthConfiguration(
int $invalidDepth,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf('Invalid recursion depth configuration: %d (%s)', $invalidDepth, $reason);
return self::withContext($message, [
'error_type' => 'invalid_configuration',
'invalid_depth' => $invalidDepth,
'reason' => $reason,
'valid_range' => 'Depth must be a positive integer between 1 and 1000',
], 0, $previous);
}
/**
* Create an exception with recommendations for handling deep structures.
*
* @param int $currentDepth The current recursion depth
* @param int $maxDepth The maximum allowed recursion depth
* @param string $path The field path where the issue occurred
* @param array<string> $recommendations List of recommendations
* @param Throwable|null $previous Previous exception for chaining
*/
public static function withRecommendations(
int $currentDepth,
int $maxDepth,
string $path,
array $recommendations,
?Throwable $previous = null
): static {
$message = sprintf(
"Recursion depth limit reached at path '%s' (depth: %d/%d)",
$path,
$currentDepth,
$maxDepth
);
return self::withContext($message, [
'error_type' => 'depth_with_recommendations',
'current_depth' => $currentDepth,
'max_depth' => $maxDepth,
'field_path' => $path,
'recommendations' => $recommendations,
], 0, $previous);
}
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when rule execution fails.
*
* This exception is thrown when:
* - Conditional rules fail during execution
* - Rule callbacks throw errors
* - Rule evaluation encounters runtime errors
* - Custom masking logic fails
* - Rule processing exceeds limits
*
* @api
*/
class RuleExecutionException extends GdprProcessorException
{
/**
* Create an exception for conditional rule execution failure.
*
* @param string $ruleName The rule that failed
* @param string $reason The reason for failure
* @param mixed $context Additional context about the failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forConditionalRule(
string $ruleName,
string $reason,
mixed $context = null,
?Throwable $previous = null
): static {
$message = sprintf("Conditional rule '%s' execution failed: %s", $ruleName, $reason);
$contextData = [
'rule_name' => $ruleName,
'reason' => $reason,
'category' => 'conditional_rule',
];
if ($context !== null) {
$contextData['context'] = $context;
}
return self::withContext($message, $contextData, 0, $previous);
}
/**
* Create an exception for callback execution failure.
*
* @param string $callbackName The callback that failed
* @param string $fieldPath The field path being processed
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forCallback(
string $callbackName,
string $fieldPath,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf(
"Callback '%s' failed for field path '%s': %s",
$callbackName,
$fieldPath,
$reason
);
return self::withContext($message, [
'callback_name' => $callbackName,
'field_path' => $fieldPath,
'reason' => $reason,
'category' => 'callback_execution',
], 0, $previous);
}
/**
* Create an exception for rule timeout.
*
* @param string $ruleName The rule that timed out
* @param float $timeoutSeconds The timeout threshold in seconds
* @param float $actualTime The actual execution time
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forTimeout(
string $ruleName,
float $timeoutSeconds,
float $actualTime,
?Throwable $previous = null
): static {
$message = sprintf(
"Rule '%s' execution timed out after %.3f seconds (limit: %.3f seconds)",
$ruleName,
$actualTime,
$timeoutSeconds
);
return self::withContext($message, [
'rule_name' => $ruleName,
'timeout_seconds' => $timeoutSeconds,
'actual_time' => $actualTime,
'category' => 'timeout',
], 0, $previous);
}
/**
* Create an exception for rule evaluation error.
*
* @param string $ruleName The rule that failed evaluation
* @param mixed $inputData The input data being evaluated
* @param string $reason The reason for evaluation failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forEvaluation(
string $ruleName,
mixed $inputData,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Rule '%s' evaluation failed: %s", $ruleName, $reason);
return self::withContext($message, [
'rule_name' => $ruleName,
'input_data' => $inputData,
'reason' => $reason,
'category' => 'evaluation',
], 0, $previous);
}
}

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Ivuorinen\MonologGdprFilter\Exceptions;
use Throwable;
/**
* Exception thrown when Laravel service registration fails.
*
* This exception is thrown when:
* - Service provider fails to register GDPR processor
* - Configuration publishing fails
* - Logging channel registration fails
* - Artisan command registration fails
* - Service binding or resolution fails
*
* @api
*/
class ServiceRegistrationException extends GdprProcessorException
{
/**
* Create an exception for channel registration failure.
*
* @param string $channelName The channel that failed to register
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forChannel(
string $channelName,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Failed to register GDPR processor with channel '%s': %s", $channelName, $reason);
return self::withContext($message, [
'channel_name' => $channelName,
'reason' => $reason,
'category' => 'channel_registration',
], 0, $previous);
}
/**
* Create an exception for service binding failure.
*
* @param string $serviceName The service that failed to bind
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forServiceBinding(
string $serviceName,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Failed to bind service '%s': %s", $serviceName, $reason);
return self::withContext($message, [
'service_name' => $serviceName,
'reason' => $reason,
'category' => 'service_binding',
], 0, $previous);
}
/**
* Create an exception for configuration publishing failure.
*
* @param string $configPath The configuration path that failed
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forConfigPublishing(
string $configPath,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Failed to publish configuration to '%s': %s", $configPath, $reason);
return self::withContext($message, [
'config_path' => $configPath,
'reason' => $reason,
'category' => 'config_publishing',
], 0, $previous);
}
/**
* Create an exception for command registration failure.
*
* @param string $commandClass The command class that failed to register
* @param string $reason The reason for failure
* @param Throwable|null $previous Previous exception for chaining
*/
public static function forCommandRegistration(
string $commandClass,
string $reason,
?Throwable $previous = null
): static {
$message = sprintf("Failed to register command '%s': %s", $commandClass, $reason);
return self::withContext($message, [
'command_class' => $commandClass,
'reason' => $reason,
'category' => 'command_registration',
], 0, $previous);
}
}