PHPackages                             v-woody/laravel-data-masking - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. v-woody/laravel-data-masking

ActiveLibrary[Security](/categories/security)

v-woody/laravel-data-masking
============================

Automatically mask sensitive fields in logs, model serialisation, and API responses for Laravel.

v1.1.0(2w ago)09MITPHPPHP ^8.2CI passing

Since May 22Pushed 2w agoCompare

[ Source](https://github.com/v-Woody/laravel-data-masking)[ Packagist](https://packagist.org/packages/v-woody/laravel-data-masking)[ Docs](https://github.com/v-Woody/laravel-data-masking)[ RSS](/packages/v-woody-laravel-data-masking/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (6)Versions (3)Used By (0)

Laravel Data Masking
====================

[](#laravel-data-masking)

Automatically mask sensitive fields in Eloquent model serialisation, API resources, and log context. Built for PII compliance (GDPR, CCPA, HIPAA) without requiring you to change how you use your models.

Requirements
------------

[](#requirements)

- PHP 8.2 or higher
- Laravel 10, 11, 12, or 13

Installation
------------

[](#installation)

```
composer require v-woody/laravel-data-masking
```

The service provider and facade are registered automatically via Laravel's package discovery.

Optionally publish the config file:

```
php artisan vendor:publish --tag=data-masking-config
```

How It Works
------------

[](#how-it-works)

When a model serialises to an array (via `toArray()`, `toJson()`, or a JSON API response), the package intercepts the output and replaces sensitive field values with masked equivalents. The same masking can be applied to Laravel log context entries via a Monolog processor.

Masking rules can be defined in three ways, applied in priority order:

1. **PHP 8 attributes** on model properties (highest priority)
2. **`MasksFields` interface** returning an array of field definitions
3. **Config file rules** per model class (lowest priority)

Usage
-----

[](#usage)

### Option 1: PHP 8 Attributes

[](#option-1-php-8-attributes)

Add an attribute directly to a model property. No other configuration is needed.

```
use VWoody\DataMasking\Concerns\HasMaskedAttributes;
use VWoody\DataMasking\Attributes\MaskEmail;
use VWoody\DataMasking\Attributes\MaskPhone;
use VWoody\DataMasking\Attributes\MaskName;

class User extends Model
{
    use HasMaskedAttributes;

    #[MaskEmail]
    public string $email;

    #[MaskPhone]
    public string $phone;

    #[MaskName]
    public string $full_name;
}
```

```
$user->email;        // jamie@example.com  (original, direct access)
$user->toArray();    // ['email' => 'j****@*******.com', ...]
```

### Option 2: MasksFields Interface

[](#option-2-masksfields-interface)

Implement `MasksFields` and return a list of field definitions. Each entry is either `'field:MaskerClass'` or just `'field'` (which uses `StringMasker` as the default).

```
use VWoody\DataMasking\Concerns\HasMaskedAttributes;
use VWoody\DataMasking\Contracts\MasksFields;
use VWoody\DataMasking\Maskers\EmailMasker;
use VWoody\DataMasking\Maskers\PhoneMasker;

class User extends Model implements MasksFields
{
    use HasMaskedAttributes;

    public function maskedFields(): array
    {
        return [
            'email:' . EmailMasker::class,
            'phone:' . PhoneMasker::class,
        ];
    }
}
```

### Option 3: Config File

[](#option-3-config-file)

Define masking rules per model in `config/data-masking.php`. Useful when you cannot or do not want to modify the model class directly.

```
'models' => [
    App\Models\User::class => [
        'email'       => \VWoody\DataMasking\Maskers\EmailMasker::class,
        'phone'       => \VWoody\DataMasking\Maskers\PhoneMasker::class,
        'card_number' => \VWoody\DataMasking\Maskers\CardNumberMasker::class,
    ],
],
```

### Custom Masker

[](#custom-masker)

Create a class implementing the `Masker` contract to define your own masking logic.

```
use VWoody\DataMasking\Contracts\Masker;

class NationalInsuranceMasker implements Masker
{
    public function mask(string $value): string
    {
        return substr($value, 0, 2) . '****' . substr($value, -1);
    }
}
```

You can then reference it anywhere a masker class is expected:

```
#[Mask(NationalInsuranceMasker::class)]
public string $ni_number;
```

Or use `CustomMasker` inline for one-off programmatic use:

```
use VWoody\DataMasking\Maskers\CustomMasker;

$masker = new CustomMasker(fn (string $value) => str_repeat('*', strlen($value)));
$masker->mask('secret'); // '******'
```

Note: `CustomMasker` requires a `Closure` and cannot be resolved from the container, so it is not suitable for use in config or attributes. Create a dedicated class for those cases.

Built-in Maskers
----------------

[](#built-in-maskers)

ClassExample inputExample output`EmailMasker``jamie@example.com``j****@*******.com``PhoneMasker``+44 7911 123456``+** **** **3456``NameMasker``Jamie Woodruff``J**** W*******``CardNumberMasker``4111 1111 1111 1234``**** **** **** 1234``IpAddressMasker``192.168.1.100``192.168.*.*``StringMasker``mysecret``m*******``CustomMasker`any stringclosure-defined outputAPI Resource Masking
--------------------

[](#api-resource-masking)

Extend `MaskedJsonResource` instead of `JsonResource` to apply masking automatically when your model is serialised through an API resource.

```
use VWoody\DataMasking\Resources\MaskedJsonResource;

class UserResource extends MaskedJsonResource
{
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}
```

Masking rules are resolved from the underlying `$this->resource` model using the same attribute, interface, and config priority as everywhere else.

Log Masking
-----------

[](#log-masking)

Add the `MaskingTap` to any logging channel in `config/logging.php`:

```
'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single'],
        'tap' => [\VWoody\DataMasking\Log\MaskingTap::class],
    ],
],
```

Then define which context fields to mask in `config/data-masking.php`:

```
'log_fields' => [
    'email'      => \VWoody\DataMasking\Maskers\EmailMasker::class,
    'ip_address' => \VWoody\DataMasking\Maskers\IpAddressMasker::class,
    'password'   => \VWoody\DataMasking\Maskers\StringMasker::class,
],
```

Any log call that includes these keys in its context will have them masked automatically, including nested arrays.

```
Log::info('User logged in', ['email' => 'jamie@example.com']);
// Logs: User logged in {"email":"j****@*******.com"}
```

Bypassing Masking
-----------------

[](#bypassing-masking)

### Gate-based bypass (recommended)

[](#gate-based-bypass-recommended)

Define a gate name in the config. When the gate passes for the current user, masking is skipped. This is the recommended approach for admin panels or internal tooling.

```
// config/data-masking.php
'bypass_gate' => 'view-unmasked-data',
```

```
// App\Providers\AuthServiceProvider
Gate::define('view-unmasked-data', fn (User $user) => $user->isAdmin());
```

### Callback bypass

[](#callback-bypass)

Wrap any code in `DataMasking::unmasked()` to disable masking for the duration of that callback, regardless of gate state.

```
use VWoody\DataMasking\Facades\DataMasking;

$rawData = DataMasking::unmasked(fn () => $user->toArray());
```

Note: when running under Laravel Octane or Swoole, `DataMasking::unmasked()` is safe to use because the service is scoped per request.

Testing
-------

[](#testing)

```
composer test
```

Linting
-------

[](#linting)

```
composer lint
```

Artisan Commands
----------------

[](#artisan-commands)

### data-masking:verify

[](#data-maskingverify)

Inspect which fields will be masked on a given model and which masker and source applies to each.

```
php artisan data-masking:verify "App\Models\User"
```

Example output:

```
Masking rules for [App\Models\User]:

+---------+------------------------------------------+-----------+
| Field   | Masker                                   | Source    |
+---------+------------------------------------------+-----------+
| email   | VWoody\DataMasking\Maskers\EmailMasker   | attribute |
| phone   | VWoody\DataMasking\Maskers\PhoneMasker   | attribute |
| ni      | App\Maskers\NationalInsuranceMasker      | config    |
+---------+------------------------------------------+-----------+

Total: 3 field(s) will be masked.

```

The Source column tells you where the rule came from: `attribute` (PHP attribute on the property), `interface` (the `MasksFields` interface), or `config` (the config file).

Contributing
------------

[](#contributing)

Contributions are welcome. Please follow these steps:

1. Fork the repository on GitHub.
2. Create a branch for your change: `git checkout -b feature/your-feature-name`
3. Write tests for your change. All existing tests must continue to pass.
4. Run the test suite: `composer test`
5. Run the linter and fix any issues: `composer lint`
6. Open a pull request against the `main` branch with a clear description of what the change does and why.

### Guidelines

[](#guidelines)

- Follow the coding standards used throughout the package (PSR-12, Spatie Laravel guidelines).
- Keep pull requests focused. One feature or fix per PR.
- Do not break backwards compatibility without discussion in an issue first.
- New maskers should implement the `Masker` contract and include unit tests covering normal input, edge cases, and empty or short values.

### Reporting Issues

[](#reporting-issues)

Open an issue on GitHub. Include a description of the problem, the Laravel and PHP version you are using, and a minimal reproduction case where possible.

Security
--------

[](#security)

If you discover a security vulnerability, please report it privately by emailing  rather than opening a public issue. Do not disclose the details publicly until a fix has been released.

Licence
-------

[](#licence)

The MIT Licence. See the [LICENCE](LICENCE) file for details.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance96

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

18d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/42e18f5d663ebf26d26c41657488df88d1de73b52b4ae848cb3ae6602cbf0b5b?d=identicon)[v-Woody](/maintainers/v-Woody)

---

Top Contributors

[![v-Woody](https://avatars.githubusercontent.com/u/80185125?v=4)](https://github.com/v-Woody "v-Woody (8 commits)")

---

Tags

laravelgdprprivacypiiMaskingdata-masking

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/v-woody-laravel-data-masking/health.svg)

```
[![Health](https://phpackages.com/badges/v-woody-laravel-data-masking/health.svg)](https://phpackages.com/packages/v-woody-laravel-data-masking)
```

###  Alternatives

[devrabiul/laravel-cookie-consent

A GDPR-compliant cookie consent solution for Laravel applications with fully customizable cookie banners, granular consent control, and enterprise-grade compliance features.

18148.5k1](/packages/devrabiul-laravel-cookie-consent)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
