PHPackages                             ivanbaric/sanigen - 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. [Validation &amp; Sanitization](/categories/validation)
4. /
5. ivanbaric/sanigen

ActiveLibrary[Validation &amp; Sanitization](/categories/validation)

ivanbaric/sanigen
=================

Declarative sanitization and attribute generators for Eloquent models.

v1.9.0(1w ago)04MITPHPPHP ^8.2CI failing

Since Jul 17Pushed 1w agoCompare

[ Source](https://github.com/IvanBaric/sanigen)[ Packagist](https://packagist.org/packages/ivanbaric/sanigen)[ RSS](/packages/ivanbaric-sanigen/feed)WikiDiscussions master Synced 1w ago

READMEChangelog (10)Dependencies (18)Versions (12)Used By (0)

Sanigen
=======

[](#sanigen)

[![Latest Version on Packagist](https://camo.githubusercontent.com/9e0e7636b8c0d3ab561ceb02adde60de05506417e908df296c14bafe515da31d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6976616e62617269632f73616e6967656e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ivanbaric/sanigen)[![Total Downloads](https://camo.githubusercontent.com/1479c2a565ab68ea14173c303f9911e1b5d00197c9499f70df20900c1207dd7a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6976616e62617269632f73616e6967656e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ivanbaric/sanigen)[![License](https://camo.githubusercontent.com/98b1a5a382f97b8f0a5fae3ba8a32032f9ce1bf07b9bc5590903f3d82d57cb23/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6976616e62617269632f73616e6967656e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/ivanbaric/sanigen)

Sanigen provides declarative sanitization and attribute generators for Laravel Eloquent models.

Quick Start
-----------

[](#quick-start)

```
composer require ivanbaric/sanigen
php artisan vendor:publish --provider="IvanBaric\Sanigen\SanigenServiceProvider" --tag="config"
```

```
use Illuminate\Database\Eloquent\Model;
use IvanBaric\Sanigen\Traits\Sanigen;

class Post extends Model
{
    use Sanigen;

    protected array $sanitize = [
        'title' => 'text',
        'description' => 'plain_text',
        'content' => 'safe_html',
        'email' => 'email',
        'website' => 'url',
        'price' => 'decimal',
    ];
}
```

Sanitization runs when an attribute is assigned through the Eloquent model. All sanitizer rules automatically recurse through arrays while preserving integer, float, boolean, null values, keys, and structure.

Sanitizer classes still receive and return one string. Sanigen's structured engine owns recursion, path matching, type preservation, limits, and conflict handling.

Structured Values
-----------------

[](#structured-values)

### Homogeneous JSON

[](#homogeneous-json)

A root rule is the default for every string in that attribute:

```
protected array $sanitize = [
    'translations' => 'text',
];

$model->translations = [
    'hr' => 'Naslov',
    'en' => 'Title',
    'published' => true,
    'revision' => 3,
];
```

The two strings are cleaned while `true` remains a boolean and `3` remains an integer. Arrays may be nested to any configured depth.

### Heterogeneous JSON

[](#heterogeneous-json)

Use dot notation when different fields need different pipelines:

```
protected array $sanitize = [
    'settings.title' => 'text',
    'settings.email' => 'email',
    'settings.price' => 'decimal',
];
```

Only existing matching values are sanitized. Missing paths are a no-op, keys are never created, and unrelated fields remain unchanged. If a path ends at an array, its pipeline becomes the default for every string in that subtree.

### Wildcards

[](#wildcards)

`*` matches one existing array key, including numeric and associative keys:

```
protected array $sanitize = [
    'settings.contacts.*.name' => 'text',
    'settings.contacts.*.email' => 'email',
    'settings.groups.*.members.*.name' => 'text',
];
```

Multiple wildcard levels are supported. Empty arrays and wildcards without matches are no-ops.

Invalid paths fail before the value is written. Empty segments, partial wildcards such as `cont*cts`, and a wildcard root such as `*.email` are rejected.

### Rule Precedence

[](#rule-precedence)

Each string leaf uses exactly one pipeline:

1. The longer matching path wins.
2. At equal length, the path with more literal segments wins.
3. A root rule is the default for its entire structure.
4. A specific child rule overrides that default.
5. Equal-specificity matches with different pipelines throw an exception.

Declaration order never changes the result:

```
protected array $sanitize = [
    'settings' => 'text',
    'settings.email' => 'email',
    'settings.contacts.*' => 'text',
    'settings.contacts.*.email' => 'email',
];
```

Sanigen groups these rules under `settings` and traverses that root value once.

Spatie Translatable
-------------------

[](#spatie-translatable)

Install Spatie's package and use Sanigen's integration trait instead of importing two conflicting `setAttribute` traits:

```
composer require spatie/laravel-translatable
```

```
use Illuminate\Database\Eloquent\Model;
use IvanBaric\Sanigen\Traits\HasSanitizedTranslations;

class Page extends Model
{
    use HasSanitizedTranslations;

    public array $translatable = ['title', 'content'];

    protected array $sanitize = [
        'title' => 'text',
        'content' => 'safe_html',
    ];
}

$page->title = [
    'hr' => 'Hrvatski naslov',
    'en' => 'English title',
];

$page->content = [
    'hr' => 'Hrvatski alert(1)',
    'en' => 'English alert(1)',
];
```

Every locale is sanitized automatically. The bridge preserves Spatie's normal JSON storage and translation reads while ensuring Sanigen receives the complete value before it is stored.

Standard Aliases
----------------

[](#standard-aliases)

The shipped aliases are config-driven and may be replaced in `config/sanigen.php`:

```
'aliases' => [
    'text' => 'unicode|strip_html|strip_emoji|strip_newlines|trim|squish',
    'plain_text' => 'unicode|strip_html|strip_emoji|normalize_newlines|trim',
    'title' => 'unicode|strip_html|strip_emoji|strip_newlines|trim|squish|lower|ucfirst',
    'ascii' => 'unicode|strip_html|strip_emoji|strip_newlines|trim|squish|ascii|trim',
    'safe_html' => 'unicode|safe_html',
    'email' => 'trim|lower|email',
    'url' => 'trim|strip_newlines|url',
    'slug' => 'trim|lower|slug',
    'decimal' => 'trim|decimal',
    'phone' => 'trim|phone_clean',
],
```

- `text` produces one line of ordinary text and normalizes whitespace.
- `plain_text` keeps intentional line breaks and normalizes `\r\n` and `\r` to `\n`.
- `safe_html` retains allowed rich HTML after parser-based sanitization.

The `unicode` primitive performs only Unicode NFC normalization. It does not transliterate Croatian letters (`č ć ž š đ Č Ć Ž Š Đ`), guess legacy encodings, or repair mojibake. Invalid Unicode fails closed.

The `normalize_newlines` primitive only converts Windows and old Mac line endings to `\n`.

Decimal Values
--------------

[](#decimal-values)

`decimal` normalizes textual user input:

```
12,50 €      -> 12.50
1.234,56 €   -> 1234.56
1,234.56 USD -> 1234.56
-12,50 €     -> -12.50

```

Inside arrays, integers remain integers, floats remain floats, booleans remain booleans, and null remains null.

Sanigen normalizes input. Laravel validation decides whether a value is allowed. An Eloquent cast decides how it is stored and presented. `decimal` is not a validation rule.

Deprecated `recursive:` Prefix
------------------------------

[](#deprecated-recursive-prefix)

Since every rule now recurses automatically, these declarations are equivalent:

```
'settings' => 'text',
'settings' => 'recursive:text',
```

`recursive:` remains accepted for applications upgrading from 1.8.0, but it is deprecated and may be removed in a future major release. New code should use the ordinary rule. An empty `recursive:` expression still throws a clear exception. No runtime deprecation warning is emitted.

Rule Sources
------------

[](#rule-sources)

Rules can come from three places, in this priority order:

1. Model properties (`$sanitize`, `$generate`)
2. Class-level attributes (`#[Sanitize]`, `#[Generate]`)
3. Config defaults (`sanitize_defaults`, `generate_defaults`)

```
use IvanBaric\Sanigen\Attributes\Generate;
use IvanBaric\Sanigen\Attributes\Sanitize;

#[Sanitize(['title' => 'text', 'email' => 'email'])]
#[Generate(['slug' => 'slugify:title', 'uuid' => 'uuid'])]
class Post extends Model
{
    use Sanigen;
}
```

Sanigen never infers sanitizer rules from database column types.

Built-in Sanitizers
-------------------

[](#built-in-sanitizers)

SanitizerPurpose`unicode`Normalize valid Unicode to NFC`normalize_newlines`Convert `\r\n` and `\r` to `\n``trim`, `lower`, `upper`, `ucfirst`, `squish`Text transformations`strip_newlines`, `strip_html`, `strip_tags`, `strip_emoji`Plain-text cleanup`safe_html`, `strip_scripts`Parser-based HTML sanitization`alpha`, `alnum`, `alpha_dash`, `ascii`, `digits`Character filtering`decimal`, `email`, `phone_clean`, `url`, `slug`Format normalization`strip_tags` is a compatibility wrapper around PHP's `strip_tags()` and is not an XSS boundary. Use `safe_html` for rich HTML that will be rendered unescaped.

Custom Sanitizers and Aliases
-----------------------------

[](#custom-sanitizers-and-aliases)

Every custom sanitizer handles one string:

```
namespace App\Sanitizers;

use IvanBaric\Sanigen\Sanitizers\Contracts\Sanitizer;

final class UsernameSanitizer implements Sanitizer
{
    public function apply(string $value): string
    {
        return strtolower(trim($value));
    }
}
```

```
use IvanBaric\Sanigen\Registries\SanitizerRegistry;

SanitizerRegistry::register('username', \App\Sanitizers\UsernameSanitizer::class);
```

Aliases can contain built-in sanitizers, custom sanitizers, or other aliases. Circular aliases fail clearly. An alias may share a name with a base sanitizer, as the shipped `safe_html` alias does.

```
php artisan make:sanitizer Username
php artisan make:sanitizer Admin/TitleClean --force
```

Generators
----------

[](#generators)

The generator API is unchanged:

```
protected array $generate = [
    'uuid' => 'uuid:v7',
    'slug' => 'slugify:title',
    'code' => 'unique_string:10',
    'expires_at' => 'carbon:+7 days',
    'owner_id' => 'user:id',
];
```

Built-in generators include `uuid`, `ulid`, `autoincrement`, `unique_string`, `random_string`, `slugify`, `carbon`, and `user`.

Custom generators continue to use `GeneratorRegistry::register()` and `GeneratorContract`:

```
php artisan make:generator CouponCode
```

Database unique indexes remain the final authority for values that must be unique under concurrent requests.

Security Model
--------------

[](#security-model)

Sanigen is one layer in the input lifecycle:

1. Laravel validation rejects disallowed input.
2. Sanigen cleans and normalizes values assigned through Eloquent.
3. Eloquent casts control storage and presentation types.
4. Blade escaping protects ordinary output.

Keep Blade escaping enabled for ordinary text:

```
{{ $post->description }}
```

Render unescaped content only when it is intentionally sanitized with `safe_html`:

```
{!! $post->content !!}
```

Every root traversal enforces:

- `max_nested_depth`
- `max_nested_items`
- `max_scalar_input_length`
- `max_html_input_length` and sanitizer-specific limits

The item counter is shared across the complete root operation. Scalar length is checked before a sanitizer pipeline runs. Objects and resources fail closed with the root and nested path in the exception, without including the submitted value. Sanitization builds a copy and does not partially write an attribute when processing fails.

Sanitizer failures default to:

```
'failure_mode' => 'throw',
```

Supported modes are `throw`, `null`, and `original`. `original` is a compatibility mode that may preserve unsafe input and should be used only with an explicit migration plan. Missing sanitizers separately support `throw`, `ignore`, and `log`; `throw` is the default.

Existing Rows
-------------

[](#existing-rows)

`sanitizeAttributes()` processes each unique top-level attribute once and returns whether anything changed.

The resanitize command applies current rules to stored models:

```
php artisan sanigen:resanitize "App\Models\Post" --chunk=200
php artisan sanigen:resanitize "App\Models\Post" --dry-run
```

This command updates records. Test it on staging and use a backup-aware deployment path.

Limitations
-----------

[](#limitations)

Sanigen runs only when data passes through an Eloquent model or `sanitizeAttributes()` is called. Raw SQL, Query Builder updates, and bulk operations that bypass model assignment are not sanitized.

Development
-----------

[](#development)

```
composer install
composer test
vendor/bin/pint --test
vendor/bin/phpstan analyse
composer audit
```

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance98

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity56

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 ~42 days

Recently: every ~32 days

Total

10

Last Release

12d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/8364e79aacbf982a433dbe7f978eacd6987572355a0cf762d69a35a7c4b73e72?d=identicon)[IvanBaric](/maintainers/IvanBaric)

---

Top Contributors

[![IvanBaric](https://avatars.githubusercontent.com/u/6261128?v=4)](https://github.com/IvanBaric "IvanBaric (31 commits)")

---

Tags

laravellaravel-frameworklaravel-packagelaravel12

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ivanbaric-sanigen/health.svg)

```
[![Health](https://phpackages.com/badges/ivanbaric-sanigen/health.svg)](https://phpackages.com/packages/ivanbaric-sanigen)
```

###  Alternatives

[illuminate/validation

The Illuminate Validation package.

18838.8M2.0k](/packages/illuminate-validation)[sandermuller/laravel-fluent-validation

Fluent validation rule builders for Laravel

21428.9k6](/packages/sandermuller-laravel-fluent-validation)

PHPackages © 2026

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