mrgoodbytes8667/string-mask-bundle
A Symfony bundle that adds string masking utilities to your application. Hide or partially redact sensitive values such as emails, phone numbers, and card numbers, and reuse the masking service across templates, logs, and API responses.
This is a Symfony/Twig bundle, not a Laravel package. Before writing any code, confirm what you're actually getting: composer require mrgoodbytes8667/string-mask-bundle will install a bundle whose service definitions, Twig extension, and configuration tree are wired through Symfony's DI container and bundle extension (DependencyInjection/*Extension.php). Laravel will not auto-discover it, will not run its compiler passes, and will not publish its config. So "getting started" for a Laravel developer is really a three-step triage:
Inspect the source first. Open vendor/mrgoodbytes8667/string-mask-bundle/src/. Look for two things:
StringMasker / a service implementing a MaskInterface). This is the only part that is likely framework-agnostic and reusable in Laravel.src/Twig/…Extension.php). This is where the filter name (mask, string_mask, etc.) and its argument signature live — read it to learn the exact mask character and "keep N leading/trailing characters" semantics.As of v1.1.0 the internals were run through rector and php-cs-fixer, so class names, namespaces, and method signatures may have shifted from what you saw in 1.0.1. If you're holding a snippet copied from an older version, re-grep src/ for the FQCN rather than trusting it.
Decide whether you need the bundle at all. If the masking logic is a handful of mb_substr/str_repeat calls, porting it into a 20-line app/Support/StringMask.php is cheaper and safer than carrying a Symfony bundle with no Laravel integration. Use the bundle directly only if the masking rules (per-type masks for email/phone/card/IBAN, configurable reveal counts) are non-trivial and well-tested.
First use case: mask PII at the presentation boundary — e.g. an email in a Blade view or a log line — not in the database. Something like:
// app/Support/StringMask.php — a thin adapter if you do reuse the bundle class
use MrGoodBytes8667\StringMaskBundle\...\StringMasker; // confirm actual FQCN in src/ for v1.1.0
public function mask(string $value): string
{
return app(StringMasker::class)->mask($value); // or new StringMasker(...)
}
Wire it in AppServiceProvider::register() with a bind() if the class has constructor dependencies from Symfony config. That's your whole "integration."
Version note: v1.1.0 requires PHP 8.2+ and Symfony 6.4+. That's a good fit alongside Laravel 11/12 (also PHP 8.2+), so the old worry about pulling in a package pinned to ancient Symfony is gone — but it also means anyone on PHP 8.1/Laravel 10 must stay on 1.0.1, whose API may differ.
Bind the masker once, inject everywhere. In AppServiceProvider::register():
$this->app->singleton(\App\Support\Masker::class, function () {
return new \App\Support\Masker(
maskChar: '*',
keepLeft: 2,
keepRight: 2,
);
});
Then inject into controllers, jobs, and API resources. Avoid new-ing it inline in views.
Prefer your own adapter class over the bundle class. Given the rector pass between 1.0.1 and 1.1.0, keep App\Support\Masker as the only place that references the vendor FQCN. If v1.2 renames things or you port the logic wholesale, one file changes and nothing else. This is cheap insurance on a single-maintainer package.
Blade equivalent of the Twig filter. The bundle gives you a Twig filter; you need a Blade directive or a Str macro. Prefer the macro so it composes with Str::of():
// AppServiceProvider::boot()
Str::macro('mask', function (string $value) {
return app(\App\Support\Masker::class)->mask($value);
});
{{ Str::of($user->email)->mask() }}
{{-- or a directive: @mask($user->email) --}}
API resources — mask in toArray(), keep raw on the model. This is the pattern that pays off daily:
public function toArray($request): array
{
return [
'id' => $this->id,
'email' => $this->masked_email, // accessor that calls the masker
'phone' => $this->masked_phone,
];
}
Keep the raw column intact for search, auth, and uniqueness checks. Masking is a view concern, not a storage concern.
Monolog processor for PII redaction. This is where a masking service earns its keep:
// app/Logging/MaskPiiProcessor.php
public function __invoke(array $record): array
{
$record['context'] = array_map(
fn ($v) => is_string($v) ? $this->masker->mask($v) : $v,
$record['context']
);
return $record;
}
Tap it in config/logging.php under the channel's tap array. This is more valuable than any Blade filter — logs leak PII constantly.
Support/debugging endpoints. A ?include_masked=1 toggle, or a dedicated masked field alongside the real one for support staff with the right permission. Don't build a global "mask everything" switch; make it per-field and explicit.
Compliance/export flows. When generating CSV/PDF exports for users, mask third-party PII (other customers' emails on shared reports) but not the requesting user's own data. This is the one place masking genuinely matters beyond cosmetics.
config/*.php publish, no auto-discovery (extra.laravel won't be present). You are writing the glue. v1.1.0 did nothing to change this — it's a Symfony-side housekeeping release. Budget for the glue work; it's not composer require and go.^1.0.1 and accept that its internals differ from the current docs and source listing. There are no new features here, so nothing is lost by staying back — but also nothing is gained by upgrading beyond Symfony/PHP alignment.?string → string). Diff src/ between the two tags before bumping, and check your adapter still type-checks. If you've already copied the masking logic into app/Support/, this is a non-event.NOASSERTION. This is still the biggest risk and v1.1.0 does not change it. It means the package didn't declare a standard SPDX license. You cannot assume MIT. Get legal sign-off before shipping it in a commercial product — or, better, port the ~50 lines of masking logic and attribute it, which sidesteps the question for internal use while keeping provenance.src/. Check composer.json for the PHP constraint (>=8.2 as of v1.1.0) and abandoned dependencies.substr() bugs. Verify the implementation uses mb_* functions. Test with Zoë@example.com, a 4-byte emoji, and an empty string. Short inputs ("ab") often throw or return the original — decide which behavior you want and guard at the call site. Add these cases to your adapter's tests so a future upgrade that "cleans up" the logic can't silently regress them.j***@e***.com still leaks enough to correlate records. If you need GDPR/PCI compliance, you need tokenization or hashing, not character substitution. Don't let a mask helper create false confidence.mrgoodbytes_string_mask: in YAML) will never be read in Laravel. Whatever defaults the bundle ships with are what you get unless you instantiate the class yourself with explicit arguments. Replicate the settings in config/string_mask.php and pass them in. The v1.1.0 cleanup may have touched default constructor arguments — re-read the constructor signature after upgrading rather than assuming your defaults still apply.*** and rejects. Order matters: validate raw, mask at output.$value into a caught exception message).twigbridge to Laravel, the Twig filter may just work — but test the filter registration, since the bundle's extension is normally registered via Symfony's twig.extension tag, and the v1.1.0 refactor is exactly the kind of change that can perturb how it's wired.How can I help you explore Laravel packages today?