Data Masking

The core hides sensitive values before they are written to logs, debug panels, and other places where data may be exposed to third parties. DataMasker recursively processes arrays, objects, and strings according to key and regex rules.

Registration: DataMaskerServiceProvider in HTTP providers (before LogServiceProvider, so the logger receives the masker). Default rules live in config/masking.php (see configuration, Configuration → masking.php).

Architecture

ClassRole
DataMaskerInterfaceContract: mask(), clearRules()
DataMaskerRule orchestrator, recursion over structures
RegexDataMaskerRuleRegex for strings and key patterns (from config/masking.php)
DataMaskerRuleInterfaceCustom rule (apply, isSensitiveKey)

By default DataMaskerServiceProvider creates a DataMasker with one RegexDataMaskerRule using patterns from configuration. The service is a shared singleton.

config/masking.php

In the skeleton, config/masking.php defines three groups of settings. DataMaskerServiceProvider reads them via ConfigInterface (ConfigKey::MASKING_PATTERNS, MASKING_KEY_PATTERNS, MASKING_RULES) and builds one RegexDataMaskerRule plus additional classes from masking.rules:

<?php declare(strict_types=1);

return [
    'masking' => [
        // Regex for strings (log messages, scalar values)
        'patterns' => [
            '/[a-z0-9_\-\+\.]+@[a-z0-9\-]+\.[a-z]{2,}/i' => '***@***.***',
            '/\d{4}-\d{4}-\d{4}-\d{4}/' => '****-****-****-****',
            '/(password|token|api_key|...)[:=]+([^\s,;]+)/i' => '$1=*****',
        ],
        // Regex for key names in arrays/objects → value becomes ***
        'key_patterns' => [
            '/.*password.*/i',
            '/.*token.*/i',
            '/.*_csrf_token.*/i',
            '/api_key/i',
        ],
        // DataMaskerRuleInterface classes — resolved from DI
        'rules' => [
            // MyCustomMaskerRule::class,
        ],
    ],
];
  • patternsRegexDataMaskerRule::apply() for strings (email, cards, password=… in text)
  • key_patternsisSensitiveKey(): matching key name → DataMasker::MASK_CHARS (***)
  • rules — additional rules; class must implement DataMaskerRuleInterface

Changes to masking.php do not require PHP edits — a request restart is enough.

Default rules (skeleton)

What stock config/masking.php provides in Concept Skeleton:

Keys (key_patterns) — field value is replaced with ***:

  • names containing password, token, _csrf_token, secret
  • api_key, authorization

Strings (patterns) — regex replacement in text:

  • email → ***@***.***
  • card number 9999-9999-9999-9999 → asterisks
  • pairs like password=..., token=..., api_key=... → value replaced with *****

Logger integration

LogServiceProvider passes DataMaskerInterface to Concept\Core\Services\Logger\Logger. The Monolog processor masks $record->context before writing to file or stderr.

$logger->info('User updated', [
    'email' => 'user@example.com',
    'password' => 'secret123',  // logged as ***
]);

The message ($message) is not masked automatically — do not embed passwords or tokens in the message text; put them only in context.

Manual usage

use Concept\Core\Services\DataMasker\Contracts\DataMaskerInterface;

/** @var DataMaskerInterface $masker */
$masker = $container->get(DataMaskerInterface::class);

$safe = $masker->mask([
    'login' => 'oleh',
    'password' => 'hunter2',
    'card' => '4111-1111-1111-1111',
]);

The mask() method for arrays and objects first performs a deepClone so the original is not modified. maskRecursive() modifies the passed structure in place — useful when cloning is unnecessary.

Custom rules

Usually extending config/masking.php is enough (see above). If you need full control — in your own provider (after DataMaskerServiceProvider) you can override the binding:

use Concept\Core\Services\DataMasker\Contracts\DataMaskerInterface;
use Concept\Core\Services\DataMasker\DataMasker;
use Concept\Core\Services\DataMasker\RegexDataMaskerRule;

$masker = new DataMasker();
$masker->addRule(new RegexDataMaskerRule(
    patterns: ['/iban=\d+/i' => 'iban=***'],
    keyPatterns: ['/.*_pin$/i']
));

$container->add(DataMaskerInterface::class, fn () => $masker)->setShared(true);

Or add a class to masking.rules in configuration — the provider will wire it through DI. For more complex logic, implement DataMaskerRuleInterface.

DebugBar and other components

The DebugBar component injects DataMaskerInterface into the request collector — the panel shows already masked values for $_POST, $_SESSION, etc. Any custom diagnostic tool should be built the same way.

Masking does not replace database encryption and does not remove sensitive fields from HTTP responses. It reduces the risk of leakage through logs and dev tools.