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

Data Sanitize Demo Bundle Laravel Package

endroid/data-sanitize-demo-bundle

A lightweight Symfony demo bundle from Endroid that illustrates data sanitization in practice. It serves as a reference for cleaning and validating incoming data before it reaches your application, helping developers explore sanitization patterns and adapt them to their own PHP projects.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: it is a demo bundle, not a dependency. The repository is a Symfony bundle with a stub README (title only), 1 star, 0 dependents, and no versioning or test signal. In a Laravel codebase you will never composer require it in production — you read the source and port the pattern. Frame your first hour around extraction, not installation.

Minimal steps (spike/sandbox only)

If you genuinely want to run it to observe behavior, keep it strictly dev-scoped and in a throwaway Symfony sandbox — mixing a Symfony bundle into a Laravel app means manually registering a bundle class that Laravel's kernel does not know about, which is wasted effort:

composer require --dev endroid/data-sanitize-demo-bundle

In a Symfony sandbox you would then add it to config/bundles.php:

Endroid\DataSanitizeDemoBundle\DataSanitizeDemoBundle::class => ['dev' => true],

Then bin/console list to find the demo command. In a Laravel project, skip this entirely and go straight to reading code.

Where to look first, in order

  1. composer.json — PHP/Symfony constraints. This tells you instantly whether the demo targets PHP 5.x/7.x and Symfony 2/3/4 (likely), which means it will never coexist with Laravel 10/11 on PHP 8.1+.
  2. src/Command/ — the demo entry points; they show the intended call sequence (read → sanitize → dedupe → report/persist).
  3. src/Service/ or src/Sanitizer/ — the actual cleaning and duplicate-detection logic. This is the only part worth your time.
  4. src/DependencyInjection/ and config/services.yaml — how sanitizers are tagged and composed. This is the pattern you will replicate in Laravel's container.
  5. tests/ — if it exists at all; treat its absence as a given.

Ignore README.md; it contains nothing beyond the title.

First use case

The realistic day-to-day scenario this informs: a messy import that must be cleaned and de-duplicated before it hits your tables. CSV of customer records, a legacy table dump, or a webhook feed with inconsistent casing, padded whitespace, mixed email formats, and repeated rows. The demo shows the shape of that pipeline; you will build it with Laravel primitives:

php artisan make:command ImportCleanCustomers

with the flow: stream/chunk input → run sanitizer chain → compute dedupe keys → dry-run report → insert with upsert() behind a unique index.


Implementation Patterns

Pattern 1 — Tagged sanitizer chain (Symfony → Laravel translation)

The bundle's core teaching is a set of small, single-responsibility sanitizer services composed via a tagged iterator. In Symfony that is !tagged_iterator app.sanitizer; in Laravel you get the same result with container tagging and a lazy collection:

// AppServiceProvider::register()
$this->app->singleton(SanitizerChain::class, function ($app) {
    return new SanitizerChain([
        new TrimWhitespace(),
        new LowercaseEmail(),
        new StripControlChars(),
        new NormalizePhone('NL'),
        new CollapseWhitespace(),
    ]);
});

Or, if you want config-driven discovery, tag them and resolve:

$this->app->tag([
    TrimWhitespace::class,
    LowercaseEmail::class,
    StripControlChars::class,
], 'app.sanitizer');

$this->app->singleton(SanitizerChain::class, fn ($app) =>
    new SanitizerChain(iterator_to_array($app->tagged('app.sanitizer')))
);

Each sanitizer is one function: sanitize(mixed $value): mixed. Adding a rule means adding a class — that is the extensibility the demo is demonstrating, and it maps cleanly onto Laravel.

Pattern 2 — Pipeline instead of chain for order sensitivity

Order matters (strip control chars before collapsing whitespace; trim after concatenation). If your rules have a strict sequence, prefer Laravel's Pipeline over an unordered tagged iterator:

$clean = app(Pipeline::class)
    ->send($row)
    ->through([
        StripControlChars::class,
        TrimWhitespace::class,
        LowercaseEmail::class,
    ])
    ->thenReturn();

Keep per-field rules in a config array so the order is reviewable in one place rather than scattered across service definitions.

Pattern 3 — Dedupe strategies, in escalating cost

The demo will show naive comparison. In production, pick deliberately:

Strategy When Laravel implementation
Exact match Stable business key exists unique index + insertOrIgnore()
Normalized match Key varies by casing/whitespace/format Compute a dedupe_hash column, index it
Business-key match Composite key (email + postcode) Composite unique index
Blocking + fuzzy No reliable key Bucket on a cheap key, then similar_text/levenshtein inside the bucket

The scalable pattern is blocking: never compare all rows against all rows. Bucket on a normalized field (e.g. substr(soundex(last_name),1,4) or normalized domain), then only run fuzzy comparison within a bucket. This is what the demo almost certainly omits and what will save you at volume.

// Normalize + hash for the blocking key
$row['dedupe_hash'] = hash('xxh128', implode('|', [
    Str::lower(trim($row['email'])),
    preg_replace('/\D/', '', $row['phone'] ?? ''),
]));

Then upsert() on dedupe_hash for idempotent re-runs.

Pattern 4 — Where sanitization belongs in a Laravel request lifecycle

Do not centralize everything in one middleware. Split by concern:

  • Shape/type validation → Form Request + Validator rules (email, string, max:). Reject early.
  • Normalization → Eloquent mutators/setters or a spatie/laravel-data DTO with a from() constructor that sanitizes.
  • Uniqueness/dedupe → database unique indexes and upsert/insertOrIgnore, never an application-level exists() check (racy).
  • Bulk cleanup of existing rows → a queued job or command with chunking.
class CustomerData extends Data
{
    public function __construct(
        #[Required] public string $email,
        public string $name,
    ) {}

    public static function fromArray(array $payload): self
    {
        return new self(
            email: Str::lower(trim($payload['email'])),
            name: Str::squish($payload['name']),
        );
    }
}

Pattern 5 — The batch command (the demo's real deliverable)

Adapt the bundle's console command into an Artisan command with the affordances a demo will not have:

  • --dry-run that writes a diff report and mutates nothing
  • --limit= and chunkById(500)
  • row-level logging of what changed (old → new), into a sanitize_audit table or log channel
  • idempotency: re-running produces no further changes
  • a --source= argument instead of hardcoded fixtures

For large volumes, dispatch SanitizeBatchMessage per chunk on a queue rather than looping in-process — this is the single biggest difference between demo and production.

Pattern 6 — Evaluation harness before merge rules

If you are removing duplicates, you need precision/recall on a labeled sample before deleting anything. Build a tiny fixture set of ~50 hand-labelled pairs, run your matcher, and measure. The demo bundle has no such harness; add it before you write merge logic, because merge rules are where silent data loss lives.

Pattern 7 — Extraction as an internal package

If the rules prove out, wrap only the proven logic in an internal Composer package exposing Laravel-friendly config (config/sanitize.php publishable) and a Sanitize facade. Do not fork the demo bundle wholesale — fork the two or three classes you actually validated.


Gotchas and Tips

Do not install it into a Laravel app. Laravel's kernel does not register Symfony bundles. Any "integration" is custom glue with zero upstream support. --dev scoping is the only defensible placement, and even then a sandbox is better.

Version constraints will likely block you anyway. Demo bundles from that era typically pin PHP 5.x/7.x and Symfony 2/3/4. Check composer.json before a spike — even if Composer resolves, you can pull unsupported Symfony components transitively into your lockfile.

Demo fixtures can be loaded accidentally. If the bundle ships sample data or seeders, running its command against a real database is the worst-case failure. Always run first against a copy, never against production.

Order-of-operations bugs in sanitizer chains. Unordered tagged iterators are non-deterministic in effect. Unicode normalization (Normalizer::normalize(..., Form::NFKC)) must happen before comparison, not after; case folding must precede hashing. Write one test per rule and one integration test asserting the final shape of a known-dirty row.

Aggressive normalization causes collisions. Lowercasing, stripping punctuation, and collapsing whitespace can map genuinely distinct records onto the same key. Never put a unique index on a normalized column before you have measured collision rate on real data — add it as a non-unique index first and query for duplicates.

Silent data loss is the top risk of dedupe. Never hard-delete on a fuzzy match. Insert into a merge_candidates table with a confidence score, and require either a threshold plus audit log or a human review step. Keep the original row; soft-delete or link via merged_into_id.

Non-idempotent retries. Queue workers retry. If your sanitize job inserts, a retry duplicates rows. Guard with a unique constraint and upsert(), or a processed_at marker on the source, so a second pass is a no-op.

Encoding issues surface late. Invalid UTF-8 from CSV imports will silently truncate or produce replacement characters. Validate with mb_check_encoding($v, 'UTF-8') at ingest and convert from the source encoding explicitly; do not let MySQL's utf8 (3-byte) column quietly mangle emoji.

Memory and timeouts at scale. ->get() on the import table will OOM. chunkById() (not chunk() — the latter is unstable when you mutate or insert while iterating). For fuzzy matching, precompute a blocking key so you never do an O(n²) pass.

Database constraints are the real dedupe mechanism. Application-level if (Customer::where('email', $email)->exists()) is a race condition under concurrency. Rely on the unique index and catch QueryException / use insertOrIgnore().

Rollback plan is not optional. Before any destructive cleanup: snapshot the table, or take a full column-level copy of affected fields into an audit table, and write the reverse migration. Test the rollback on the copy.

Extension points worth copying from src/DependencyInjection/: the service tags and the compiler-pass-style composition. That is the transferable lesson — how to make sanitizers pluggable without touching the orchestrator. Everything else in the bundle is illustrative sample code.

Don't trust the README for a config reference. It is a title. There is no documented install path, no configuration schema, no upgrade policy. Anything you learn must come from reading src/.

Maintenance ownership starts day one. 1 star, 0 dependents, no tests, no semver. If any code is copied (MIT permits it), assign an owner, add tests, and put it through the same security review as first-party code. Prefer maintained, framework-agnostic libraries for normalization/similarity and keep the dedupe policy — which is genuinely yours — in-house.

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.
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json