Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Array Redactor Laravel Package

mtownsend/array-redactor

Redact sensitive data from PHP arrays before logging or storing it. Array Redactor masks values by key using dot notation and wildcards, with a configurable replacement string and Laravel support. Keeps passwords, tokens, and personal data out of your logs and dumps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer:

composer require mtownsend/array-redactor

The package is auto-discovered on Laravel 5.5+. You get a service provider (Mtownsend\ArrayRedactor\ArrayRedactorServiceProvider) and, optionally, a facade alias.

The core API is a small value-object-style class. Pass the array and the keys you want to redact, then call redact():

use Mtownsend\ArrayRedactor\ArrayRedactor;

$payload = [
    'username' => 'michael',
    'password' => 'super-secret',
    'profile' => [
        'api_token' => 'tok_abc123',
        'email' => '[email protected]',
    ],
];

$redacted = (new ArrayRedactor($payload, ['password', 'profile.api_token']))->redact();

Result:

[
    'username' => 'michael',
    'password' => '[REDACTED]',          // placeholder from config
    'profile' => [
        'api_token' => '[REDACTED]',
        'email' => '[email protected]',
    ],
]

As of 1.0.1, ink() also accepts a callback, so the replacement value can be computed per matched key instead of being a single static placeholder:

$redacted = (new ArrayRedactor($payload, ['password', 'profile.api_token']))
    ->ink(fn ($value) => is_string($value) ? str_repeat('*', min(strlen($value), 12)) : '[REDACTED]')
    ->redact();

The callback signature is the first thing to confirm in source — check src/ArrayRedactor.php to see exactly what it receives (value only, or value plus key/path in your installed version) before building on it.

First places to look in the repo: README.md for the exact placeholder and the ink() callback contract, src/ArrayRedactor.php for the matching logic (dot notation, wildcards) and where the callback is invoked, and src/ArrayRedactorServiceProvider.php for the facade binding and config merge.

The most useful first use case in a Laravel app is dropping it into your logging so request bodies never leak secrets:

Log::info('Checkout started', [
    'payload' => (new ArrayRedactor($request->all(), ['password', 'card.number', 'card.cvv']))->redact(),
]);

Implementation Patterns

Redact before logging request/response data. Wrap any Log::* call that dumps user-supplied data:

public function store(Request $request)
{
    Log::debug('Incoming user update', [
        'body' => (new ArrayRedactor($request->all(), [
            'password',
            'password_confirmation',
            'api_token',
        ]))->redact(),
    ]);
}

Use ink() callbacks for per-field redaction strategies. A single placeholder is fine for passwords but poor for tokens you need to correlate across log lines, or cards where support staff need the last four digits:

$redacted = (new ArrayRedactor($request->all(), [
    'password',
    'card.number',
    'api_token',
]))
    ->ink(function ($value) {
        // Never return the raw value — that defeats the redaction entirely.
        return is_string($value) && strlen($value) > 4
            ? str_repeat('*', strlen($value) - 4) . substr($value, -4)
            : '[REDACTED]';
    })
    ->redact();

Stable correlation via hashing. If you need to answer "same token as the previous request?" without storing the token, return a short hash from the callback:

->ink(fn ($value) => is_scalar($value) ? 'sha1:' . substr(hash('sha256', (string) $value), 0, 12) : null)

Use a keyed HMAC (hash_hmac with an app secret) rather than a bare hash for low-entropy values like PINs, or you've just published a rainbow-table-able fingerprint.

Type-preserving redaction. Callbacks let you keep null as null so downstream code that distinguishes "absent" from "present" keeps working:

->ink(fn ($value) => $value === null ? null : '[REDACTED]')

Redact queued job payloads before they hit failed_jobs or your logs. Jobs frequently carry credentials:

public function failed(Throwable $e)
{
    Log::error('Job failed', [
        'payload' => (new ArrayRedactor($this->payload, ['auth.password', 'auth.token']))->redact(),
    ]);
}

Sanitize audit trails and exception context. If you report to Sentry/Bugsnag and pass an array into report()'s context, redact first — and prefer callbacks returning fixed-length output so log/event sizes stay predictable.

Use dot notation for nested keys. profile.api_token targets nested data; a flat api_token key will not match a key buried one level down. Combine this with wildcards for collections:

$redacted = (new ArrayRedactor($team, [
    'members.*.password',
    'members.*.two_factor_secret',
]))->redact();

Wrap it in a small app-level helper so you have one place to change the key list and the callback strategy:

// app/Support/Redact.php
class Redact
{
    public static function sensitive(array $data): array
    {
        return (new ArrayRedactor($data, [
            'password',
            'password_confirmation',
            'token',
            'secret',
            'api_key',
            'authorization',
        ]))
            ->ink(fn ($value) => $value === null ? null : '[REDACTED]')
            ->redact();
    }

    public static function partially(array $data): array
    {
        return (new ArrayRedactor($data, ['card.number', 'card.cvv']))
            ->ink(fn ($value) => is_string($value) && strlen($value) > 4
                ? str_repeat('*', strlen($value) - 4) . substr($value, -4)
                : '[REDACTED]')
            ->redact();
    }
}

Then call Redact::sensitive($request->all()) everywhere instead of repeating key lists — and so the callback logic is testable in one place.

Middleware for request logging. If you have a "log all requests" middleware, run the request payload through the redactor before persisting:

$logged = (new ArrayRedactor($request->all(), config('logging.redact_keys', [])))->redact();
RequestLog::create(['path' => $request->path(), 'payload' => $logged]);

Keep the key list in config, but keep callbacks in code (see gotchas).

Gotchas and Tips

  • The 1.0.1 change is additive, not breaking. If you never call ink(), behavior is unchanged — the placeholder from config is still used. Only adopt callbacks where you actually need per-value output.

  • Verify the callback contract in source. Release notes only say "support for callbacks in the ink method"; they don't document arity, whether the key/path is passed, or what happens with array values. Read src/ArrayRedactor.php for your installed version before writing a callback that depends on a second argument.

  • A callback that returns the original value silently leaks. There is no guardrail here. Prefer the static placeholder for the common case and reserve callbacks for masking/hashing; add a test asserting the raw secret is not present in the output.

  • Never put closures in config/ files. php artisan config:cache cannot serialize closures and will fail (or silently drop them). Keep callbacks in a helper class or a registry keyed by name, and reference the name from config:

    // config/logging.php
    'redact_strategy' => 'partial_last_four', // not: fn ($v) => ...
    
  • This package is a thin utility, not a full PII framework. 1.0.1 is a small, additive release on a tiny codebase; still verify it against your PHP/Laravel version and read src/ArrayRedactor.php yourself. It has essentially no community surface for edge cases.

  • Keys are matched, not discovered. A redactor cannot find "secrets" on its own; if you add a new sensitive field, you must add it to your key list — a callback does not make matching smarter, it only changes the replacement. Centralize the list in config or a helper to avoid drift.

  • Nested keys require dot notation. password only matches top-level; user.settings.password requires the full path. Flat key lists silently miss nested data — a common source of leaks.

  • Wildcards match arrays-of-arrays, not arbitrary depth. Use items.*.token for a list of items; deep nesting often needs an explicit path per level. Confirm wildcard behavior against the source for your version, since the README is light on edge cases — especially whether a callback is invoked once per matched element or once per pattern.

  • The placeholder is configurable. Publish the config if you need to change [REDACTED] to something that fits your log format (or to a null/"***" value). Don't hardcode the placeholder anywhere — read it from config. Note it is ignored on any path where your ink() callback supplies the value, so mixed usage means two sources of truth for what a redacted value looks like.

  • Case sensitivity. Key matching is literal; Password and password are different keys. Normalize your request keys or maintain both in your redact list.

  • Numeric-keyed arrays are trickier. Indexes inside a redacted structure can shift the wildcard pattern; test against array_values-style payloads rather than assuming it works.

  • Redaction ≠ omission. The key remains, only its value changes. If downstream code branches on isset($data['password']), it will still see the key present with a placeholder — usually what you want, but be aware. A callback returning null does not remove the key either.

  • Keep callbacks deterministic, cheap, and side-effect-free. They run once per matched key, potentially on every request if you redact in middleware. No DB lookups, no Log:: calls inside the callback (you'll recurse or leak the raw value), no rand().

  • Return type is a plain array. No fluent chaining beyond ink()/redact(). If you need to feed it into a DTO or a JsonResponse, decode/encode around it explicitly. A callback that returns an array or object changes the shape of your log payload — assert on it in tests.

  • Prefer it over hand-rolled loops. A foreach over $request->all() that unsets keys is easy to get wrong (mutating by reference, nested arrays, missing keys). This package is small enough to audit and handles the traversal for you; the callback slot removes most of the remaining reasons to hand-roll.

  • Escape hatch: if the package's behavior doesn't fit, Arr::set($data, $key, $placeholder) in a loop with Arr::has() is a five-line alternative — and if you find yourself writing a large callback, that's a signal you may want the transformation in your own class regardless.

  • Testing. Because it's a deterministic array transform, assert on the full output array, including untouched keys, so a future change to your key list is caught by a failing test rather than a silent log leak. Add explicit cases for the callback paths: null values, non-string values (ints, booleans, nested arrays), and values shorter than your masking threshold.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle