diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml
index 2768d74..68ab7c9 100644
--- a/.github/workflows/php.yml
+++ b/.github/workflows/php.yml
@@ -17,8 +17,9 @@ jobs:
strategy:
matrix:
include:
- - php-version: '7.4'
- php-version: '8.1'
+ - php-version: '8.2'
+ - php-version: '8.4'
fail-fast: false
steps:
diff --git a/README.md b/README.md
index 746b862..78eaf9b 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,15 @@

-This Symfony bundle provides a monolog processor to transform log data, in order to respect GDPR or to anonymize sensitive data.
+This Symfony bundle provides a Monolog processor to transform log data in order to respect GDPR or anonymize sensitive data.
-It allows Ip anonymization, encoding or removing data in the log.
+It allows IP anonymization, hashing, encryption, or removal of sensitive fields in logs.
+
+## Requirements
+
+- PHP >= 8.1
+- Symfony 6, 7 or 8
+- Monolog 2 or 3
## Installation
@@ -14,91 +20,112 @@ composer require aubes/shadow-logger-bundle
## Configuration
-The configuration looks as follows :
-
```yaml
# config/packages/shadow-logger.yaml
shadow_logger:
- # If enabled, add "shadow-debug" on "extra" with debug information when exception occurred
- debug: '%kernel.debug%'
+ # Add "shadow-debug" in "extra" when a transformer throws an exception.
+ # Recommended to use '%kernel.debug%' so it is only active in development.
+ debug: '%kernel.debug%'
- # If enabled, remove value when exception occurred
+ # When a transformer throws an exception:
+ # true → the field value is set to null
+ # false → the original (untransformed) value is kept
strict: true
- # Register ShadowProcessor on channels or handlers, not both
- # To configure channels or handlers is recommended for performance reason
- # Logging channels the ShadowProcessor should be pushed to
+ # Register ShadowProcessor on handlers OR channels (not both).
+ # Scoping to specific handlers/channels is recommended for performance.
handlers: ['app']
-
- # Logging handlers the ShadowProcessor should be pushed to
#channels: ['app']
+ # Salt used by the "hash" transformer (see Hash transformer section).
encoder:
salt: '%env(SHADOW_LOGGER_ENCODER_SALT)%'
-
+
mapping:
- # Context fields
+ # Fields to transform in the log "context"
context:
- custom_field: [] # Array of Transformer aliases
-
- # Examples:
- user_ip: ['ip']
- user_name: ['hash']
+ user_ip: ['ip']
+ user_name: ['hash']
user_birthdate: ['remove']
- # Extra fields
+ # Fields to transform in the log "extra"
extra:
- custom_field: [] # Array of Transformer aliases
+ custom_field: ['remove']
```
-### Mapping
+## Choosing between hashing and encryption
-Field name could contain dot to dive into array.
+Both the `hash` and `encrypt` transformers protect sensitive data in logs, but they serve different purposes. Choosing the right one depends on what you need to do with the data after it is logged.
-For example, if 'extra' contains the array :
+### Hashing (`hash`)
-```php
-'user' => [
- 'id' => /* ... */,
- 'name' => [
- 'first' => /* ... */,
- 'last' => /* ... */,
- ],
-]
-```
+Hashing is a **one-way, irreversible** operation. The original value cannot be recovered from the hash.
+
+**Use it when:**
+- You do not need to read back the original value
+- You want to correlate log entries belonging to the same user (e.g. trace a user across multiple requests) without storing their identity — the same input always produces the same hash
+- You want to pseudonymize data in compliance with GDPR
+
+**Limitations:**
+- If the space of possible values is small (e.g. an IP address), an attacker with access to the logs could reconstruct the original values through brute force
+- The salt must be kept secret; rotating it means previously hashed values can no longer be correlated with new ones
+- You cannot fulfill a GDPR "right of access" request using only the hashed value
-It is possible to modify `ip` and `name` fields :
+**Configuration:**
```yaml
-# config/packages/shadow-logger.yaml
shadow_logger:
- mapping:
- extra:
- user.ip: ['ip']
- user.name.first: ['hash']
- user.name.last: ['remove']
+ encoder:
+ algo: 'sha256' # any algorithm from hash_algos()
+ salt: '%env(SHADOW_LOGGER_ENCODER_SALT)%'
+ binary: false
```
-Warning, it is better to use field name without dot for performance.
-Internally, when a field name contains a dot the PropertyAccessor is used instead of a simple array key access.
+> Always configure a `salt` to prevent rainbow table attacks. Store it as a secret environment variable.
+
+### Encryption (`encrypt`)
-## Transformer
+Encryption is a **reversible** operation. The original value can be recovered using the key and the IV stored alongside it.
-Currently, this bundle provides these transformers :
- * **ip**: Anonymize IP v4 or v6 (cf: `Symfony\Component\HttpFoundation\IpUtils::anonymize`)
- * **hash**: Encode the value using [hash](https://www.php.net/manual/fr/function.hash.php) function
- * **string**: Cast a `scalar` into `string` or call `__toString` on object
- * **remove**: Remove value (replaced by `--obfuscated--`)
- * **encrypt**: Encrypt the value (available only if encryptor is configured, cf: [Encrypt transformer](#encrypt-transformer))
+**Use it when:**
+- You may need to read back the original value later (e.g. to respond to a GDPR right of access or right of erasure request, or to debug a production issue with proper authorization)
+- You want to store sensitive data in logs in a protected form without losing it permanently
-### Chain transformers
+**Limitations:**
+- Requires secure key management: if the key is compromised, all encrypted log entries are exposed
+- Key rotation is complex: old entries encrypted with a previous key can no longer be decrypted with the new one
+- The presence of the IV in the log reveals that the original field was non-empty, which may itself be sensitive
+- More computationally expensive than hashing
-You can chain transformers, for example to encode a "Stringable" object :
+### Quick comparison
+
+| | `hash` | `encrypt` | `remove` |
+|---|---|---|---|
+| Reversible | No | Yes (with key) | No |
+| Correlate entries | Yes (same input → same output) | No (IV is random) | No |
+| Protect against brute force | With a strong salt | Yes | Yes |
+| GDPR right of access | No | Yes | No |
+| Key management required | Salt only | Key + IV | None |
+
+## Transformers
+
+The following transformers are available out of the box:
+
+| Alias | Description |
+|-------|-------------|
+| `ip` | Anonymizes an IPv4 or IPv6 address |
+| `hash` | Hashes the value using the configured algorithm |
+| `string` | Casts a scalar or `Stringable` object to string |
+| `remove` | Replaces the value with `--obfuscated--` |
+| `encrypt` | Encrypts the value (requires encryptor configuration) |
+| `truncate` | Masks the middle of a value, keeping start and/or end visible (requires truncator configuration) |
+
+### Chaining transformers
+
+Transformers can be chained. They are applied in order, the output of one becoming the input of the next. For example, to hash a `Stringable` object:
```yaml
-# config/packages/shadow-logger.yaml
shadow_logger:
- # [...]
mapping:
context:
custom_field: ['string', 'hash']
@@ -106,24 +133,26 @@ shadow_logger:
### Hash transformer
-Encoder configuration :
+The `hash` transformer uses the configured encoder. See the [Hashing section](#hashing-hash) above for configuration options.
+
+### Encrypt transformer
+
+The `encrypt` transformer supports two modes: a built-in encryptor based on OpenSSL, or a custom implementation.
+
+#### Built-in encryptor
```yaml
-# config/packages/shadow-logger.yaml
shadow_logger:
- # [...]
- encoder:
- algo: 'sha256' # cf: https://www.php.net/manual/fr/function.hash-algos.php
- salt: '%env(SHADOW_LOGGER_ENCODER_SALT)%'
- binary: false
+ encryptor:
+ key: '%env(SHADOW_LOGGER_ENCRYPTOR_KEY)%'
+ cipher: 'aes-256-cbc' # optional, default: aes-256-cbc
```
-### Encrypt transformer
+The key must be kept secret and should be provided via an environment variable. The cipher can be any algorithm supported by OpenSSL (`openssl_get_cipher_methods()`).
-The bundle does not provide an encryption class.
-To use the "encrypt" transformer, you need to manually configure the encryptor.
+#### Custom encryptor
-First, you need to create an Adapter class and extends [EncryptorInterface](src/Encryptor/EncryptorInterface.php) :
+If you need a different encryption strategy, implement [`EncryptorInterface`](src/Encryptor/EncryptorInterface.php):
```php
// src/Encryptor/EncryptorAdapter.php
@@ -133,25 +162,21 @@ use Aubes\ShadowLoggerBundle\Encryptor\EncryptorInterface;
class EncryptorAdapter implements EncryptorInterface
{
- // [...]
-
public function encrypt(string $data, string $iv): string
{
- // [...]
-
+ // your encryption logic
return $encryptedValue;
}
public function generateIv(): string
{
- // [...]
-
+ // generate a random IV
return $iv;
}
}
```
-Next, register your class as a service (if service discovery is not used):
+Register it as a service (if not using autoconfiguration):
```yaml
# config/services.yaml
@@ -159,44 +184,114 @@ services:
App\Encryptor\EncryptorAdapter: ~
```
-Finally, configure your service Id in the ShadowLoggerBundle :
+Then reference it by its service ID:
```yaml
-# config/packages/shadow-logger.yaml
shadow_logger:
- # [...]
encryptor: 'App\Encryptor\EncryptorAdapter'
```
-This transformer replaces the value with an array :
+#### Output format
+
+In both cases, the transformer replaces the field value with:
```php
[
- 'iv' => , // Random IV used to encrypt the value
- 'value' => , // Encrypted value
+ 'iv' => 'abc123', // base64-encoded IV used during encryption
+ 'value' => '...', // encrypted value
]
```
+### Truncate transformer
+
+The `truncate` transformer masks the middle of a value while keeping a configurable number of characters visible at the start and/or end. It is useful for partially revealing values like card numbers, email addresses, or tokens.
+
+Named variants are declared under `truncators`. Each variant becomes available as a transformer alias: `default` → `truncate`, others → `truncate_{name}`.
+
+```yaml
+shadow_logger:
+ truncators:
+ default: # alias: "truncate"
+ keep_start: 2
+ keep_end: 2
+ mask: '***'
+ card: # alias: "truncate_card"
+ keep_start: 4
+ keep_end: 4
+ mask: '****'
+ email: # alias: "truncate_email"
+ keep_start: 1
+ keep_end: 0
+ mask: '***'
+
+ mapping:
+ context:
+ card_number: ['truncate_card'] # 4242424242424242 → 4242****4242
+ email: ['truncate_email'] # john@example.com → j***
+ token: ['truncate'] # abcdef1234 → ab***34
+```
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `keep_start` | Number of characters to keep at the beginning | `2` |
+| `keep_end` | Number of characters to keep at the end | `2` |
+| `mask` | String used to replace the hidden part | `***` |
+
+> If the value is shorter than or equal to `keep_start + keep_end`, it is replaced entirely by the mask.
+
+## Mapping
+
+### Nested fields
+
+Field names can use dot notation to target nested array keys.
+
+Given the following `extra` structure:
+
+```php
+'user' => [
+ 'id' => 42,
+ 'name' => [
+ 'first' => 'John',
+ 'last' => 'Doe',
+ ],
+ 'ip' => '1.2.3.4',
+]
+```
+
+You can map nested fields like this:
+
+```yaml
+shadow_logger:
+ mapping:
+ extra:
+ user.ip: ['ip']
+ user.name.first: ['hash']
+ user.name.last: ['remove']
+```
+
+> **Note:** Dot notation uses the Symfony PropertyAccessor internally, which is slower than direct key access. Prefer flat field names when possible.
+
## Custom transformer
-First you need to create a Transformer class and extends [TransformerInterface](src/Transformer/TransformerInterface.php) :
+Implement [`TransformerInterface`](src/Transformer/TransformerInterface.php):
```php
// src/Transformer/CustomTransformer.php
namespace App\Transformer;
+use Aubes\ShadowLoggerBundle\Transformer\TransformerInterface;
+
class CustomTransformer implements TransformerInterface
{
- public function transform($data)
+ public function transform(mixed $data): mixed
{
- // [...]
-
- return $value;
+ // transform and return the value
+ return $data;
}
}
```
-Next, register your class as a service with 'shadow_logger.transformer' tag :
+Register it as a service with the `shadow_logger.transformer` tag and an `alias`:
```yaml
# config/services.yaml
@@ -205,3 +300,12 @@ services:
tags:
- { name: 'shadow_logger.transformer', alias: 'custom' }
```
+
+The `alias` is the name used in the `mapping` configuration:
+
+```yaml
+shadow_logger:
+ mapping:
+ context:
+ some_field: ['custom']
+```
diff --git a/composer.json b/composer.json
index 40f926a..222ccb3 100644
--- a/composer.json
+++ b/composer.json
@@ -10,19 +10,20 @@
}
],
"require": {
- "php": ">=7.4",
+ "php": ">=8.1",
+ "ext-openssl": "*",
"monolog/monolog": "^2.0 | ^3.0",
"symfony/polyfill-php80": "^1.0",
- "symfony/http-foundation": "^5.4 |^6.0",
- "symfony/http-kernel": "^5.4 |^6.0",
- "symfony/property-access": "^5.4 | ^6.0"
+ "symfony/http-foundation": "^6.0 | ^7.0 | ^8.0",
+ "symfony/http-kernel": "^6.0 | ^7.0 | ^8.0",
+ "symfony/property-access": "^6.0 | ^7.0 | ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.1",
"phpmd/phpmd": "^2.10",
"phpunit/phpunit": ">=9.6",
"phpspec/prophecy-phpunit": ">=v2.0.1",
- "vimeo/psalm": "^5.9"
+ "vimeo/psalm": "^5.9 | ^6.0"
},
"autoload": {
"psr-4": { "Aubes\\ShadowLoggerBundle\\": "src" },
diff --git a/config/services.yaml b/config/services.yaml
index e8bf26b..ec0e5e0 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -24,6 +24,11 @@ services:
tags:
- { name: 'shadow_logger.transformer', alias: 'string' }
+ Aubes\ShadowLoggerBundle\Encryptor\DefaultEncryptor:
+ arguments:
+ $key: ''
+ $cipher: 'aes-256-cbc'
+
Aubes\ShadowLoggerBundle\Visitor\ArrayKeyVisitor: ~
Aubes\ShadowLoggerBundle\Visitor\PropertyAccessorVisitor:
arguments:
diff --git a/psalm.xml b/psalm.xml
index 6a2b6bb..7da896b 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -13,4 +13,11 @@
+
+
+
+
+
+
+
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 568e4d6..6bad0d6 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -7,22 +7,20 @@
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
-class Configuration implements ConfigurationInterface
+final class Configuration implements ConfigurationInterface
{
/**
- * {@inheritdoc}
- *
* @psalm-suppress UndefinedMethod
*/
- public function getConfigTreeBuilder()
+ public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('shadow_logger');
$rootNode = $treeBuilder->getRootNode();
$rootNode
->children()
- ->scalarNode('debug')->defaultFalse()->info('Debug mode: add debug information when an exception occurred')->end()
- ->scalarNode('strict')->defaultTrue()->info('Strict mode: remove value when an exception occurred')->end()
+ ->booleanNode('debug')->defaultFalse()->info('Debug mode: add debug information when an exception occurred')->end()
+ ->booleanNode('strict')->defaultTrue()->info('Strict mode: remove value when an exception occurred')->end()
->arrayNode('encoder')
->children()
->scalarNode('algo')->defaultValue('sha256')->end()
@@ -30,7 +28,34 @@ public function getConfigTreeBuilder()
->booleanNode('binary')->defaultFalse()->end()
->end()
->end()
- ->scalarNode('encryptor')->end()
+ ->arrayNode('encryptor')
+ ->info('Use a service ID (string) or configure the built-in encryptor (key/cipher)')
+ ->beforeNormalization()
+ ->ifString()
+ ->then(static fn (string $v) => ['service' => $v])
+ ->end()
+ ->validate()
+ ->ifTrue(static fn ($v) => $v['service'] !== null && $v['key'] !== null)
+ ->thenInvalid('You cannot specify both "service" and "key" for the encryptor.')
+ ->end()
+ ->children()
+ ->scalarNode('service')->defaultNull()->end()
+ ->scalarNode('key')->defaultNull()->end()
+ ->scalarNode('cipher')->defaultValue('aes-256-cbc')->end()
+ ->end()
+ ->end()
+ ->arrayNode('truncators')
+ ->info('Named truncator variants, each available as a "truncate_{name}" transformer alias ("default" → "truncate")')
+ ->useAttributeAsKey('name')
+ ->arrayPrototype()
+ ->addDefaultsIfNotSet()
+ ->children()
+ ->integerNode('keep_start')->defaultValue(2)->min(0)->end()
+ ->integerNode('keep_end')->defaultValue(2)->min(0)->end()
+ ->scalarNode('mask')->defaultValue('***')->end()
+ ->end()
+ ->end()
+ ->end()
->arrayNode('channels')
->info('Logging channel list the ShadowProcessor should be pushed to')
->scalarPrototype()->end()
diff --git a/src/DependencyInjection/ShadowLoggerExtension.php b/src/DependencyInjection/ShadowLoggerExtension.php
index 9b9cc21..cb3683c 100644
--- a/src/DependencyInjection/ShadowLoggerExtension.php
+++ b/src/DependencyInjection/ShadowLoggerExtension.php
@@ -5,10 +5,13 @@
namespace Aubes\ShadowLoggerBundle\DependencyInjection;
use Aubes\ShadowLoggerBundle\Encoder\Encoder;
+use Aubes\ShadowLoggerBundle\Encryptor\DefaultEncryptor;
use Aubes\ShadowLoggerBundle\Logger\DataTransformer;
use Aubes\ShadowLoggerBundle\Logger\LogRecordShadowProcessor;
use Aubes\ShadowLoggerBundle\Logger\ShadowProcessor;
use Aubes\ShadowLoggerBundle\Transformer\EncryptTransformer;
+use Aubes\ShadowLoggerBundle\Transformer\TruncateTransformer;
+use Aubes\ShadowLoggerBundle\Truncator\Truncator;
use Aubes\ShadowLoggerBundle\Visitor\ArrayKeyVisitor;
use Aubes\ShadowLoggerBundle\Visitor\PropertyAccessorVisitor;
use Monolog\LogRecord;
@@ -22,11 +25,8 @@
/**
* @SuppressWarnings(PMD.CouplingBetweenObjects)
*/
-class ShadowLoggerExtension extends Extension
+final class ShadowLoggerExtension extends Extension
{
- /**
- * {@inheritdoc}
- */
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
@@ -37,6 +37,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->loadEncoder($config, $container);
$this->loadEncryptor($config, $container);
+ $this->loadTruncators($config, $container);
$this->loadProcessor($config, $container);
}
@@ -66,16 +67,31 @@ protected function loadProcessor(array $config, ContainerBuilder $container): vo
$this->loadTransformers($config, $container, $processor);
}
+ /**
+ * @SuppressWarnings(PMD.ElseExpression)
+ */
public function loadEncryptor(array $config, ContainerBuilder $container): void
{
- if (!isset($config['encryptor'])) {
+ if (!isset($config['encryptor']) || ($config['encryptor']['service'] === null && $config['encryptor']['key'] === null)) {
$container->removeDefinition(EncryptTransformer::class);
+ $container->removeDefinition(DefaultEncryptor::class);
return;
}
$encryptTransformer = $container->getDefinition(EncryptTransformer::class);
- $encryptTransformer->setArgument('$encryptor', new Reference($config['encryptor']));
+
+ if ($config['encryptor']['service'] !== null) {
+ $encryptorRef = new Reference($config['encryptor']['service']);
+ } else {
+ $container->getDefinition(DefaultEncryptor::class)
+ ->setArgument('$key', $config['encryptor']['key'])
+ ->setArgument('$cipher', $config['encryptor']['cipher']);
+
+ $encryptorRef = new Reference(DefaultEncryptor::class);
+ }
+
+ $encryptTransformer->setArgument('$encryptor', $encryptorRef);
}
/**
@@ -128,6 +144,27 @@ protected function loadTransformers(array $config, ContainerBuilder $container,
}
}
+ protected function loadTruncators(array $config, ContainerBuilder $container): void
+ {
+ foreach ($config['truncators'] as $name => $truncatorConfig) {
+ $alias = $name === 'default' ? 'truncate' : 'truncate_' . $name;
+
+ $truncatorId = 'shadow_logger.truncator.' . $name;
+ $transformerId = 'shadow_logger.truncate_transformer.' . $name;
+
+ $container->register($truncatorId, Truncator::class)
+ ->setArguments([
+ $truncatorConfig['keep_start'],
+ $truncatorConfig['keep_end'],
+ $truncatorConfig['mask'],
+ ]);
+
+ $container->register($transformerId, TruncateTransformer::class)
+ ->setArgument('$truncator', new Reference($truncatorId))
+ ->addTag('shadow_logger.transformer', ['alias' => $alias]);
+ }
+ }
+
protected function loadEncoder(array $config, ContainerBuilder $container): void
{
if (empty($config['encoder'])) {
@@ -135,9 +172,9 @@ protected function loadEncoder(array $config, ContainerBuilder $container): void
}
$encoder = $container->getDefinition(Encoder::class);
- $encoder->setArgument('$algo', $config['algo']);
- $encoder->setArgument('$salt', $config['salt']);
- $encoder->setArgument('$binary', $config['binary']);
+ $encoder->setArgument('$algo', $config['encoder']['algo']);
+ $encoder->setArgument('$salt', $config['encoder']['salt']);
+ $encoder->setArgument('$binary', $config['encoder']['binary']);
}
protected function propertyAccessorArray(string $path): string
diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php
index 9c66bca..bdf7c0a 100644
--- a/src/Encoder/Encoder.php
+++ b/src/Encoder/Encoder.php
@@ -4,21 +4,16 @@
namespace Aubes\ShadowLoggerBundle\Encoder;
-class Encoder implements EncoderInterface
+final class Encoder implements EncoderInterface
{
- protected string $algo;
- protected string $salt;
- protected bool $binary;
-
- public function __construct(string $algo = 'sha256', string $salt = '', bool $binary = false)
- {
+ public function __construct(
+ private readonly string $algo = 'sha256',
+ private readonly string $salt = '',
+ private readonly bool $binary = false,
+ ) {
if (!\in_array($algo, \hash_algos())) {
throw new \InvalidArgumentException('Invalid algo');
}
-
- $this->algo = $algo;
- $this->salt = $salt;
- $this->binary = $binary;
}
public function hash(string $data): string
diff --git a/src/Encryptor/DefaultEncryptor.php b/src/Encryptor/DefaultEncryptor.php
new file mode 100644
index 0000000..a597bd7
--- /dev/null
+++ b/src/Encryptor/DefaultEncryptor.php
@@ -0,0 +1,39 @@
+cipher);
+
+ if ($length === false) {
+ throw new \RuntimeException(\sprintf('Could not determine IV length for cipher "%s".', $this->cipher));
+ }
+
+ return \base64_encode(\random_bytes($length));
+ }
+
+ public function encrypt(string $data, string $iv): string
+ {
+ $encrypted = \openssl_encrypt($data, $this->cipher, $this->key, 0, \base64_decode($iv));
+
+ if ($encrypted === false) {
+ throw new \RuntimeException('Encryption failed.');
+ }
+
+ return $encrypted;
+ }
+}
diff --git a/src/Logger/DataTransformer.php b/src/Logger/DataTransformer.php
index 79fc783..9327c2e 100644
--- a/src/Logger/DataTransformer.php
+++ b/src/Logger/DataTransformer.php
@@ -8,17 +8,12 @@
class DataTransformer
{
- protected string $field;
- protected LoggerVisitorInterface $visitor;
- protected array $transformers = [];
- protected bool $strict;
-
- public function __construct(string $field, LoggerVisitorInterface $visitor, array $transformers, bool $strict)
- {
- $this->field = $field;
- $this->visitor = $visitor;
- $this->transformers = $transformers;
- $this->strict = $strict;
+ public function __construct(
+ private readonly string $field,
+ private readonly LoggerVisitorInterface $visitor,
+ private readonly array $transformers,
+ private readonly bool $strict,
+ ) {
}
public function transform(array &$record): void
diff --git a/src/Logger/LogRecordShadowProcessor.php b/src/Logger/LogRecordShadowProcessor.php
index ebe96db..e557757 100644
--- a/src/Logger/LogRecordShadowProcessor.php
+++ b/src/Logger/LogRecordShadowProcessor.php
@@ -9,7 +9,7 @@
/**
* Logger processor for Monolog 3.
*/
-class LogRecordShadowProcessor
+final class LogRecordShadowProcessor
{
use ShadowProcessorTrait;
@@ -26,6 +26,10 @@ public function __invoke(LogRecord $record): LogRecord
$this->applyTransformers($dataTransformers, $data, $property);
}
+ if (isset($data['extra']) && !isset($this->mapping['extra'])) {
+ $data['extra'] = \array_merge((array) $record['extra'], $data['extra']);
+ }
+
return $record->with(...$data);
}
}
diff --git a/src/Logger/ShadowProcessor.php b/src/Logger/ShadowProcessor.php
index b120bf3..6ccf8d2 100644
--- a/src/Logger/ShadowProcessor.php
+++ b/src/Logger/ShadowProcessor.php
@@ -7,7 +7,7 @@
/**
* Logger processor for Monolog 2.
*/
-class ShadowProcessor
+final class ShadowProcessor
{
use ShadowProcessorTrait;
diff --git a/src/Logger/ShadowProcessorTrait.php b/src/Logger/ShadowProcessorTrait.php
index 6f7bb53..528f83f 100644
--- a/src/Logger/ShadowProcessorTrait.php
+++ b/src/Logger/ShadowProcessorTrait.php
@@ -6,14 +6,11 @@
trait ShadowProcessorTrait
{
- protected bool $debug;
-
/** @var array> */
protected array $mapping = [];
- public function __construct(bool $debug)
+ public function __construct(protected readonly bool $debug)
{
- $this->debug = $debug;
}
public function addDataTransformer(string $property, DataTransformer $dataTransformer): void
diff --git a/src/Logger/TransformerException.php b/src/Logger/TransformerException.php
index 44f8cf3..a648efe 100644
--- a/src/Logger/TransformerException.php
+++ b/src/Logger/TransformerException.php
@@ -4,18 +4,14 @@
namespace Aubes\ShadowLoggerBundle\Logger;
-class TransformerException extends \RuntimeException
+final class TransformerException extends \RuntimeException
{
- protected string $field;
-
/**
* @param string $message
* @param int|mixed $code
*/
- public function __construct(string $field, $message = '', $code = 0, \Throwable $previous = null)
+ public function __construct(private readonly string $field, $message = '', $code = 0, ?\Throwable $previous = null)
{
- $this->field = $field;
-
parent::__construct($message, $code, $previous);
}
diff --git a/src/ShadowLoggerBundle.php b/src/ShadowLoggerBundle.php
index 67cfef9..e34596e 100644
--- a/src/ShadowLoggerBundle.php
+++ b/src/ShadowLoggerBundle.php
@@ -6,6 +6,6 @@
use Symfony\Component\HttpKernel\Bundle\Bundle;
-class ShadowLoggerBundle extends Bundle
+final class ShadowLoggerBundle extends Bundle
{
}
diff --git a/src/Transformer/EncryptTransformer.php b/src/Transformer/EncryptTransformer.php
index 2e3433d..48bfd0f 100644
--- a/src/Transformer/EncryptTransformer.php
+++ b/src/Transformer/EncryptTransformer.php
@@ -6,18 +6,15 @@
use Aubes\ShadowLoggerBundle\Encryptor\EncryptorInterface;
-class EncryptTransformer implements TransformerInterface
+final class EncryptTransformer implements TransformerInterface
{
- protected EncryptorInterface $encryptor;
-
- public function __construct(EncryptorInterface $encryptor)
+ public function __construct(private readonly EncryptorInterface $encryptor)
{
- $this->encryptor = $encryptor;
}
- public function transform($data): array
+ public function transform(mixed $data): array
{
- if (empty($data)) {
+ if ($data === null) {
return [];
}
diff --git a/src/Transformer/HashTransformer.php b/src/Transformer/HashTransformer.php
index ff76c39..5abdaf3 100644
--- a/src/Transformer/HashTransformer.php
+++ b/src/Transformer/HashTransformer.php
@@ -6,18 +6,15 @@
use Aubes\ShadowLoggerBundle\Encoder\EncoderInterface;
-class HashTransformer implements TransformerInterface
+final class HashTransformer implements TransformerInterface
{
- protected EncoderInterface $encoder;
-
- public function __construct(EncoderInterface $encoder)
+ public function __construct(private readonly EncoderInterface $encoder)
{
- $this->encoder = $encoder;
}
- public function transform($data): string
+ public function transform(mixed $data): string
{
- if (empty($data)) {
+ if ($data === null) {
return '';
}
diff --git a/src/Transformer/IpTransformer.php b/src/Transformer/IpTransformer.php
index 2d020e7..b0033a8 100644
--- a/src/Transformer/IpTransformer.php
+++ b/src/Transformer/IpTransformer.php
@@ -6,14 +6,12 @@
use Symfony\Component\HttpFoundation\IpUtils;
-class IpTransformer implements TransformerInterface
+final class IpTransformer implements TransformerInterface
{
/**
- * @param mixed $data
- *
* @SuppressWarnings(PMD.StaticAccess)
*/
- public function transform($data): string
+ public function transform(mixed $data): string
{
if (empty($data) || !\is_string($data)) {
throw new \InvalidArgumentException('Ip must be a string');
diff --git a/src/Transformer/RemoveTransformer.php b/src/Transformer/RemoveTransformer.php
index bcbb6b9..7dd898c 100644
--- a/src/Transformer/RemoveTransformer.php
+++ b/src/Transformer/RemoveTransformer.php
@@ -4,9 +4,9 @@
namespace Aubes\ShadowLoggerBundle\Transformer;
-class RemoveTransformer implements TransformerInterface
+final class RemoveTransformer implements TransformerInterface
{
- public function transform($data): string
+ public function transform(mixed $data): string
{
return '--obfuscated--';
}
diff --git a/src/Transformer/StringTransformer.php b/src/Transformer/StringTransformer.php
index 09de34d..302abf3 100644
--- a/src/Transformer/StringTransformer.php
+++ b/src/Transformer/StringTransformer.php
@@ -4,11 +4,11 @@
namespace Aubes\ShadowLoggerBundle\Transformer;
-class StringTransformer implements TransformerInterface
+final class StringTransformer implements TransformerInterface
{
- public function transform($data): string
+ public function transform(mixed $data): string
{
- if (empty($data)) {
+ if ($data === null) {
return '';
}
diff --git a/src/Transformer/TransformerInterface.php b/src/Transformer/TransformerInterface.php
index 3920273..7ef30a7 100644
--- a/src/Transformer/TransformerInterface.php
+++ b/src/Transformer/TransformerInterface.php
@@ -7,9 +7,7 @@
interface TransformerInterface
{
/**
- * @param mixed $data
- *
* @return array|scalar
*/
- public function transform($data);
+ public function transform(mixed $data): mixed;
}
diff --git a/src/Transformer/TruncateTransformer.php b/src/Transformer/TruncateTransformer.php
new file mode 100644
index 0000000..67b0b91
--- /dev/null
+++ b/src/Transformer/TruncateTransformer.php
@@ -0,0 +1,27 @@
+truncator->truncate((string) $data);
+ }
+}
diff --git a/src/Truncator/Truncator.php b/src/Truncator/Truncator.php
new file mode 100644
index 0000000..91e1ca7
--- /dev/null
+++ b/src/Truncator/Truncator.php
@@ -0,0 +1,29 @@
+keepStart + $this->keepEnd) {
+ return $this->mask;
+ }
+
+ $start = $this->keepStart > 0 ? \mb_substr($data, 0, $this->keepStart) : '';
+ $end = $this->keepEnd > 0 ? \mb_substr($data, -$this->keepEnd) : '';
+
+ return $start . $this->mask . $end;
+ }
+}
diff --git a/src/Truncator/TruncatorInterface.php b/src/Truncator/TruncatorInterface.php
new file mode 100644
index 0000000..ab17ec4
--- /dev/null
+++ b/src/Truncator/TruncatorInterface.php
@@ -0,0 +1,10 @@
+accessor = $accessor;
}
public function has(array $record, string $field): bool
@@ -25,10 +22,7 @@ public function has(array $record, string $field): bool
}
}
- /**
- * @return mixed
- */
- public function get(array $record, string $field)
+ public function get(array $record, string $field): mixed
{
return $this->accessor->getValue($record, $field);
}
@@ -36,7 +30,7 @@ public function get(array $record, string $field)
/**
* @psalm-suppress ReferenceConstraintViolation
*/
- public function set(array &$record, string $field, $value): void
+ public function set(array &$record, string $field, mixed $value): void
{
try {
$this->accessor->setValue($record, $field, $value);
diff --git a/tests/Transformer/EncryptTransformerTest.php b/tests/Transformer/EncryptTransformerTest.php
index 09989b3..5e6db17 100644
--- a/tests/Transformer/EncryptTransformerTest.php
+++ b/tests/Transformer/EncryptTransformerTest.php
@@ -31,15 +31,27 @@ public function testTransform()
$this->assertSame('encrypted', $result['value']);
}
- public function testTransformEmpty()
+ public function testTransformNull()
{
$encryptor = $this->prophesize(EncryptorInterface::class);
$transformer = new EncryptTransformer($encryptor->reveal());
- $this->assertSame([], $transformer->transform(''));
$this->assertSame([], $transformer->transform(null));
}
+ public function testTransformEmptyString()
+ {
+ $encryptor = $this->prophesize(EncryptorInterface::class);
+ $encryptor->generateIv()->willReturn('initialVector');
+ $encryptor->encrypt('', 'initialVector')->willReturn('encrypted');
+
+ $transformer = new EncryptTransformer($encryptor->reveal());
+ $result = $transformer->transform('');
+
+ $this->assertSame('initialVector', $result['iv']);
+ $this->assertSame('encrypted', $result['value']);
+ }
+
public function testTransformNotScalar()
{
$encryptor = $this->prophesize(EncryptorInterface::class);
diff --git a/tests/Transformer/HashTransformerTest.php b/tests/Transformer/HashTransformerTest.php
index 32cf63e..a404345 100644
--- a/tests/Transformer/HashTransformerTest.php
+++ b/tests/Transformer/HashTransformerTest.php
@@ -26,15 +26,23 @@ public function testTransform()
$this->assertSame('encoded', $transformer->transform(true));
}
- public function testTransformEmpty()
+ public function testTransformNull()
{
$encoder = $this->prophesize(EncoderInterface::class);
$transformer = new HashTransformer($encoder->reveal());
- $this->assertSame('', $transformer->transform(''));
$this->assertSame('', $transformer->transform(null));
}
+ public function testTransformEmptyString()
+ {
+ $encoder = $this->prophesize(EncoderInterface::class);
+ $encoder->hash('')->willReturn('encoded');
+
+ $transformer = new HashTransformer($encoder->reveal());
+ $this->assertSame('encoded', $transformer->transform(''));
+ }
+
public function testTransformNotScalar()
{
$encoder = $this->prophesize(EncoderInterface::class);
diff --git a/tests/Transformer/StringTransformerTest.php b/tests/Transformer/StringTransformerTest.php
index 17ba9fc..3e6f479 100644
--- a/tests/Transformer/StringTransformerTest.php
+++ b/tests/Transformer/StringTransformerTest.php
@@ -18,7 +18,7 @@ public function testTransform()
$this->assertSame('123', $transformer->transform(123));
$this->assertSame('1.23', $transformer->transform(1.23));
$this->assertSame('1', $transformer->transform(true));
- $this->assertSame('Stringable, I am', $transformer->transform(new class() {
+ $this->assertSame('Stringable, I am', $transformer->transform(new class {
public function __toString()
{
return 'Stringable, I am';