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

Serializer Bundle Laravel Package

ivory/serializer-bundle

Symfony bundle that adds serializer integration to your project, with service configuration and convenient APIs for converting PHP objects to and from JSON, XML, and other formats. Use it to centralize serialization setup and keep controllers free of format-specific code.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. ivory/serializer-bundle is a Symfony bundle. Laravel has no bundle registration pipeline, so the shipped artifact — the IvorySerializerBundle class, the DependencyInjection/ extension, and the service XML — is dead weight in a Laravel app. If you composer require ivory/serializer-bundle, you get nothing wired up. What you actually adopt is the underlying library:

composer require ivory/serializer

That library is standalone PHP and works fine outside Symfony. The bundle's value to us is as a reference for how to map declarative type config onto a serializer, not as an installable package.

Requirements by format

  • JSON: ext-json (already on any Laravel box)
  • XML: ext-simplexml + ext-libxml
  • YAML: symfony/yaml (already a transitive Laravel dependency)
  • CSV: nothing extra

Where to look first

  1. The upstream ivory/serializer README — the bundle is a thin DI wrapper, so all real behavior (context keys, mapping config, normalizer selection) lives there.
  2. The bundle's Resources/config/ service definitions to see how each format's encoder/decoder is constructed — this is the only thing worth reading from the package itself; it tells you exactly which library classes and constructor args to bind yourself.
  3. Your own config/serializer.php (we create it) — if a format isn't configured, its encoder doesn't exist and calls for that format throw.

First use case (the 80% case) — CSV → DTOs in a service provider

// app/Providers/SerializerServiceProvider.php
public function register(): void
{
    $this->app->singleton(SerializerInterface::class, function ($app) {
        $registry = new Registry();
        $registry->addEncoder(new JsonEncoder());
        $registry->addEncoder(new CsvEncoder(['delimiter' => ',', 'enclosure' => '"']));
        $registry->addDecoder(new JsonDecoder());
        $registry->addDecoder(new CsvDecoder(['delimiter' => ',']));

        return new Serializer(
            $registry,
            new ObjectNormalizer(new ClassMetadataFactory(
                new TypeParser(),
                new ArrayCollection()
            ))
        );
    });
}

Then resolve it anywhere:

public function store(SerializerInterface $serializer, Request $request)
{
    $rows = $serializer->deserialize(
        $request->getContent(),
        ProductDto::class,
        'csv',
        ['csv_delimiter' => ';']
    );

    return ProductResource::collection(collect($rows));
}

First use case, actually worth shipping: a one-off import command or admin uploader where you need CSV/JSON/XML/YAML behind one API and you don't want to write four different parsers in a week.

Implementation Patterns

Bind once, inject everywhere. Never app(SerializerInterface::class) inside controllers — inject it. This lets you swap in a stub in tests and, more importantly, lets you later replace the Ivory implementation with a Laravel-native adapter behind the same interface without touching call sites.

Wrap the library behind your own contract. You are going to want this by month two, when you've forked the library for a bug fix. Define it now:

namespace App\Support\Serialization;

interface DocumentParser
{
    public function parse(string $payload, string $format, string $dtoClass, array $context = []): array;
}

final class IvoryDocumentParser implements DocumentParser { /* wraps ivory */ }

Every controller and job depends on DocumentParser, not Ivory\Serializer\SerializerInterface. When the library stops working (it will), the swap is a one-line binding.

Format dispatch by file extension or content type. Route imports through a single entry point that picks the format, so the caller never hardcodes 'csv' vs 'json':

$format = match ($upload->getClientOriginalExtension()) {
    'csv'  => 'csv',
    'json' => 'json',
    'xml'  => 'xml',
    'yaml', 'yml' => 'yaml',
    default => throw new UnsupportedFormatException($upload->getClientOriginalExtension()),
};

DTOs are hydration targets, not Eloquent models. Ivory's normalizer uses PropertyAccess-style getters/setters (or public properties, depending on config). Eloquent models have magic accessors that will confuse it. Use plain DTOs:

final class ProductDto
{
    public string $sku;
    public string $name;
    public float $price;
    public array $tags = [];   // give arrays/collections defaults
}

Then map to models explicitly. This is where validation belongs too — see below.

Pipeline the flow: parse → validate → persist.

$dtos = $parser->parse($content, $format, ProductDto::class);

$valid = Validator::make(
    collect($dtos)->map(fn ($d) => (array) $d)->all(),
    ['*.sku' => 'required|string|max:64', '*.price' => 'required|numeric|min:0']
);

if ($valid->fails()) {
    return back()->withErrors($valid->errors());
}

The package does not validate. It will happily hydrate a DTO with price = 0 from a blank CSV cell. Treat validation as a mandatory second stage, not optional.

Chunk large CSVs at the boundary, not the parser. The library reads the whole document into memory. For anything user-uploaded, split the file into row-range chunks (e.g. 5000 rows) before handing each chunk to the serializer, and process each chunk inside a queued job. That keeps the memory profile bounded without relying on the library to stream — which it can't.

Test-fixture generation is a genuinely good fit. For seeding tests or generating demo data, the same serializer can round-trip arrays of DTOs from JSON fixtures into typed objects without you writing per-fixture hydration code. Use it here even if you never adopt it on the ingest path.

Gotchas and Tips

Composer resolution will fight you. The library's constraint ranges target Symfony 2/3-era components. On a modern Laravel (10/11, pulling Symfony 6/7), expect conflicts on symfony/serializer, symfony/yaml, and possibly php. --ignore-platform-reqs is a red flag, not a fix — if you need it to install, you can't rely on the package's internals behaving correctly at runtime.

XML is the sharpest edge. Ivory predates modern XXE hardening defaults. If you accept XML from untrusted sources — and upload features usually do — you must explicitly disable entity loading:

$previous = libxml_use_internal_errors(true);
libxml_set_external_entity_loader(fn () => null);

try {
    $result = $parser->parse($xml, 'xml', MyDto::class);
} finally {
    libxml_use_internal_errors($previous);
}

Billion-laughs and external-entity attacks are exactly the class of bug that never gets patched in an archived package. Guard it at the call site, every time, or reject XML entirely.

CSV type coercion is silent. Column price containing "abc" will hydrate as 0 or throw a confusing cast exception depending on DTO typing. Validate and assert header rows match your expected schema before deserializing:

$headers = str_getcsv($firstLine);
$expected = ['sku', 'name', 'price'];
if ($headers !== $expected) {
    throw new SchemaMismatchException(...);
}

This is the difference between a clean "your file doesn't match the template" error and a debug session at 2am.

UTF-8 BOM destroys the first column name. Excel exports CSV with \xEF\xBB\xBF. Strip it before deserializing or your first header silently becomes "\xEF\xBB\xBFsku" and the row fails to hydrate. Same for YAML files saved with CRLF.

The bundle's formats config is a trap if copied verbatim. The bundle uses its formats: block to conditionally enable encoder/decoder services. In Laravel there's no such block — you enable a format by registering the encoder in the service provider, or by not registering it. Don't try to reproduce the YAML config; just conditionally add encoders.

No package discovery, no facade, no published config. Every knob is one you wire yourself. This is fine (it's half a day of work) but it means there's no php artisan vendor:publish, no config/serializer.php shipped with defaults, and no Ivory facade. Document your own bindings carefully — the next developer won't find them by reading a README.

Archived means no patch channel. Before you ship anything depending on this, name an owner for the fork. The MIT license makes forking cheap; the commitment of maintaining a fork on the ingest path is the actual cost. If you can't name an owner, don't ship it. For a prototype or internal admin tool, it's fine.

Extend via the upstream library, not the bundle. If you need a custom normalizer (say, to map snake_case CSV columns to camelCase DTO properties), implement Ivory\Serializer\Normalizer\NormalizerInterface from the library and register it in your provider. There is no meaningful reason to touch the bundle classes at all.

The modern equivalent, if you decide against adoption. symfony/serializer (already in Laravel's dependency tree) plus CsvEncoder, XmlEncoder, and YamlEncoder covers the same ground, is maintained, and gets security patches. The migration path off Ivory — if you prototype with it and then decide to ship — is a swap of DocumentParser's implementation. Write the contract on day one so that swap stays cheap.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect