ivanbaric / sanigen
Declarative sanitization and attribute generators for Eloquent models.
Requires
- php: ^8.2
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.8
- phpstan/phpstan: ^1.10
README
Sanigen provides declarative sanitization and attribute generators for Eloquent models, so teams can keep input cleanup consistent without repeating pipelines across models.
Quick Start
composer require ivanbaric/sanigen
Recommended: publish the config file.
php artisan vendor:publish --provider="IvanBaric\Sanigen\SanigenServiceProvider" --tag="config"
Define aliases in config/sanigen.php, then reuse the same defaults across your models.
'aliases' => [ 'text' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish', 'title' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish|lower|ucfirst', 'ascii' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish|ascii', 'email' => 'trim|lower|email', 'url' => 'trim|strip_newlines|strip_scripts|url', 'slug' => 'trim|lower|slug', 'decimal' => 'trim|decimal', 'phone' => 'trim|phone_clean', ];
Sanigen gives you two model properties:
$sanitize: applies sanitizer aliases fromconfig/sanigen.php$generate: fills empty attributes oncreatingusing generator rules
use Illuminate\Database\Eloquent\Model; use IvanBaric\Sanigen\Traits\Sanigen; class Post extends Model { use Sanigen; protected $fillable = ['name', 'description', 'email', 'website', 'slug', 'price', 'phone']; protected array $sanitize = [ 'name' => 'title', 'description' => 'text', 'email' => 'email', 'website' => 'url', 'slug' => 'slug', 'price' => 'decimal', 'phone' => 'phone', ]; protected array $generate = [ 'slug' => 'slugify:name', 'uuid' => 'uuid', 'owner_id' => 'user:id', 'team_id' => 'user:current_team_id', ]; }
$post = Post::create([ 'name' => ' <script>alert(1)</script>my FIRST post ', 'description' => '<script>alert(1)</script><p>Hello <strong>world</strong></p>', 'email' => ' USER@EXAMPLE.COM ', 'website' => 'example.com', 'price' => ' EUR 1,234.56 ', 'phone' => ' +1 (123) 456-7890 ', ]);
Result:
[
'uuid' => '550e8400-e29b-41d4-a716-446655440000',
'owner_id' => 1,
'team_id' => 42,
'name' => 'My first post',
'description' => 'Hello world',
'email' => 'user@example.com',
'website' => 'https://example.com',
'slug' => 'my-first-post',
'price' => '1234.56',
'phone' => '+11234567890',
]
Available Aliases
text: generic cleaned texttitle: cleaned title-style textascii: cleaned ASCII-only textemail: normalized emailurl: normalized URLslug: slug-ready valuedecimal: normalized decimal numberphone: normalized phone number
Class-Level Attributes
Besides model properties, you can use class-level attributes:
use IvanBaric\Sanigen\Attributes\Generate; use IvanBaric\Sanigen\Attributes\Sanitize; #[Sanitize([ 'name' => 'title', 'description' => 'text', 'email' => 'email', ])] #[Generate([ 'slug' => 'slugify:name', 'uuid' => 'uuid', ])] class Post extends Model { use Sanigen; }
Rule priority is:
- explicit model properties (
$sanitize,$generate) - class-level attributes (
#[Sanitize],#[Generate]) - config defaults (
sanitize_defaults,generate_defaults)
Full Reference
Built-in sanitizers
| Sanitizer | Description | Example |
|---|---|---|
trim |
Removes whitespace from beginning and end | " Hello " -> "Hello" |
lower |
Converts to lowercase | "Hello" -> "hello" |
upper |
Converts to uppercase | "hello" -> "HELLO" |
ucfirst |
Capitalizes first character | "hello" -> "Hello" |
squish |
Normalizes whitespace to single spaces | "Hello World" -> "Hello World" |
strip_newlines |
Removes all line breaks | "Line 1\nLine 2" -> "Line 1Line 2" |
strip_html |
Removes all HTML tags | "<p>Hello</p>" -> "Hello" |
strip_tags |
Removes HTML tags except allowed ones | "<script>Hello</script>" -> "Hello" |
strip_scripts |
Removes script-bearing markup, inline JS handlers, dangerous protocols, and suspicious JS patterns | "<script>alert(1)</script>Hello" -> "Hello" |
strip_emoji |
Removes emoji characters | "Hello 👋 World 🌍" -> "Hello World " |
alpha |
Keeps only letters | "Hello123" -> "Hello" |
alnum |
Keeps only letters and numbers | "Hello123!" -> "Hello123" |
alpha_dash |
Keeps letters, numbers, hyphens, underscores | "Hello-123_!" -> "Hello-123_" |
ascii |
Keeps only ASCII characters | "cafe ć" -> "cafe " |
digits |
Keeps only digits | "Price: $123.45" -> "12345" |
decimal |
Keeps decimal number characters and normalizes separators | "1,234.56 €" -> "1234.56" |
email |
Sanitizes email addresses | " USER@EXAMPLE.COM " -> "user@example.com" |
phone_clean |
Sanitizes phone numbers | "(123) 456-7890" -> "+1234567890" |
url |
Ensures URLs have a protocol | "example.com" -> "https://example.com" |
slug |
Creates a URL-friendly slug | "Hello World" -> "hello-world" |
Built-in generators
| Generator | Purpose | Example |
|---|---|---|
uuid |
UUID v4 | 550e8400-e29b-41d4-a716-446655440000 |
uuid:v7 |
UUID v7 | 018f0f4b-9c3a-7c3e-9d9a-2c3c4b5a6d7e |
uuid:v8 |
UUID v8 | 018f0f4b-9c3a-8c3e-9d9a-2c3c4b5a6d7e |
ulid |
ULID | 01J3Z3N6K2Z9N0R2Z7T1W5Y8QG |
autoincrement |
next numeric value | 1 -> 2 -> 3 |
unique_string:length |
unique random string | unique_string:8 -> A1B2C3D4 |
random_string:length |
random string | random_string:16 -> aZ2kLm9Qp0xYt1uV |
slugify:field |
unique slug from another field | slugify:title -> my-post-title |
slugify:field,date |
unique slug with date suffix | slugify:title,date -> my-post-title-27-03-2026 |
slugify:field,uuid |
unique slug with UUID suffix | slugify:title,uuid -> my-post-title-550e8400-e29b-41d4-a716-446655440000 |
carbon:+7 days |
Carbon date from a modifier | carbon:+7 days -> 2026-04-03 12:00:00 |
user:property |
authenticated user property | user:id -> 1, user:email -> user@example.com |
Custom sanitizers
Scaffold a sanitizer class:
php artisan make:sanitizer Username php artisan make:sanitizer Admin/TitleClean --force
namespace App\Sanitizers; use IvanBaric\Sanigen\Sanitizers\Contracts\Sanitizer; class UsernameSanitizer implements Sanitizer { public function apply(string $value): string { return strtolower(trim($value)); } }
Register it:
use IvanBaric\Sanigen\Registries\SanitizerRegistry; SanitizerRegistry::register('username', \App\Sanitizers\UsernameSanitizer::class);
Use it:
protected $sanitize = [ 'username' => 'username', ];
Custom generators
Scaffold a generator class:
php artisan make:generator Slug php artisan make:generator Content/Slug --force
namespace App\Generators; use IvanBaric\Sanigen\Generators\Contracts\GeneratorContract; class CouponCodeGenerator implements GeneratorContract { public function generate(string $field, object $model): mixed { return 'SALE-' . strtoupper(str()->random(8)); } }
Register it:
use IvanBaric\Sanigen\Registries\GeneratorRegistry; GeneratorRegistry::register('coupon_code', \App\Generators\CouponCodeGenerator::class);
Use it:
protected $generate = [ 'code' => 'coupon_code', ];
Resanitize Existing Rows
Warning: this command updates existing database records. Run it on a backup-aware deployment path and test it on staging first.
php artisan sanigen:resanitize "App\Models\Post" --chunk=200
Production Notes
return [ 'enabled' => true, 'missing_sanitizer' => 'throw', 'aliases' => [ 'text' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish', 'title' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish|lower|ucfirst', 'ascii' => 'strip_html|strip_scripts|strip_emoji|strip_newlines|trim|squish|ascii', 'email' => 'trim|lower|email', 'url' => 'trim|strip_newlines|strip_scripts|url', 'slug' => 'trim|lower|slug', 'decimal' => 'trim|decimal', 'phone' => 'trim|phone_clean', ], 'allowed_html_tags' => '<p><strong><em><a><ul><ol><li><br>', 'encoding' => 'UTF-8', 'max_strip_scripts_input_length' => 32768, 'sanitize_defaults' => [], 'generate_defaults' => [], ];
Spatie Translatable Support
Sanigen works with Spatie Laravel Translatable because translatable attributes are stored as arrays and Sanigen sanitizes each scalar item individually.
Limitations
Sanigen only runs when data goes through an Eloquent model instance.
Tests
composer test
License
MIT. See LICENSE.md.