webit/doctrine-jms-json
A PHP package that bridges Doctrine with JMS serialization for JSON handling, helping you serialize and deserialize entities cleanly in Laravel or Doctrine-based applications.
webit/doctrine-jms-json gives Doctrine a custom column type that stores a PHP object graph as a JSON string, using JMS Serializer for the conversion. It is a "value object column" tool: you get a single TEXT/JSON column that round-trips an object (or an array of objects) without a join table or a hand-written json_encode/hydrate() pair.
Minimal setup (Symfony shown, but the logic is framework-agnostic):
composer require webit/doctrine-jms-json
The package dates from 2020, so check composer why-not against your doctrine/dbal and jms/serializer versions before writing any code. That compatibility check is step zero.
Register the type before Doctrine touches entity metadata. The type needs a configured JMS serializer instance, so it is a factory-built singleton rather than a plain class name:
use Doctrine\DBAL\Types\Type;
use JMS\Serializer\SerializerBuilder;
use Webit\DoctrineJmsJson\DBAL\JmsJsonTypeFactory; // confirm exact name in src/DBAL
$serializer = SerializerBuilder::create()
->addMetadataDir(__DIR__ . '/src/Domain', 'App\Domain')
->setCacheDir(__DIR__ . '/var/cache/jms')
->build();
$type = JmsJsonTypeFactory::createType($serializer /*, DefaultObjectClass::class */);
Type::addType('jms_json', $type); // DBAL 2 style; see Gotchas for DBAL 3+
Then map a property:
/** @ORM\Column(type="jms_json") */
private Address $shippingAddress;
Where to look first. In order: src/DBAL/JmsJsonType.php (read convertToDatabaseValue/convertToPHPValue — that is the package), the factory that wires the serializer in, and tests/. The README is thin and the test suite is the real specification, especially for how array element types are declared and what the default-object-class fallback does.
First use case. Pick something you'd otherwise model as an owned value object: Address, Money, OpeningHours, or a collection like array<OrderLine> that is only ever read and written as a whole. If you need to query or aggregate on a field inside it, this is the wrong tool — that's a real column.
Single immutable value object. The natural fit. Map the property, keep the VO immutable, and reassign the whole object on change:
public function changeShippingAddress(Address $address): void
{
$this->shippingAddress = $address; // replace, never mutate in place
}
Collections of objects. JMS needs a type hint to know what to instantiate on the way back. Declare it on the property docblock so the serializer's type inference picks it up:
/** @var OrderLine[] @ORM\Column(type="jms_json") */
private array $lines = [];
If your PHP version and JMS config don't provide that hint, set the default object class on the type at registration time, or wrap the collection in a dedicated OrderLines collection class registered with metadata — the wrapper is the more robust choice because it makes the type explicit at the registration site.
Polymorphism. Store a heterogeneous list (promotions, payment methods, shipping rules) by relying on JMS @Discriminator on an interface/abstract base and registering the concrete subtypes in the serializer metadata. This is the pattern that makes the package genuinely more valuable than json_encode, because JMS resolves the class back from the serialized @type.
Querying. Treat the column as opaque. You can LIKE/REGEXP it, or use native JSON functions in a raw SQL/DQL WHERE on Postgres jsonb or MySQL 5.7+, but there is no index-friendly path and no partial update. Reach for this column only for data you always load with the row.
Migrations. Let the package's SQL declaration decide the type, then verify what it emits on each platform. In practice you want JSON/jsonb on Postgres (indexable, validated) rather than TEXT; if the type emits TEXT, override getSQLDeclaration in a small subclass or set the column type explicitly in the migration while keeping the Doctrine mapping as jms_json. On MySQL, LONGTEXT is often safer than TEXT because nested objects balloon fast.
Schema evolution. Because class names and property names end up in the payload, treat the JSON as a wire format: @SerializedName, @Until/@Since versioning, and JMS class aliases are your migration tools. When you rename App\Domain\Address to App\Domain\Location, old rows still say Address — register an alias or write a one-off backfill, and do it before you ship.
Testing. Add one round-trip test per mapped type against a fixture JSON string committed to the repo. That test is what catches a JMS config change (date format, null handling) before production does. Use SQLite in-memory for the Doctrine side; the serialization behaviour is what you are actually testing.
DTO/API layering. Keep a separate serialization context (JMS groups) for the API and for persistence. Reusing one metadata set means internal fields can leak into your JSON API the moment someone adds a property annotation.
$order->getShippingAddress()->setCity(...)) may not register as a change. Always replace the object or reassign the property, and cover it with a functional test that flushes and reloads.@Version optimistic-lock column if the entity is multi-writer.Type::getName() was removed and static type registration changed. Expect to register via Type::addType('jms_json', JmsJsonType::class) plus a connection-level mapping, or via DoctrineBundle's doctrine.dbal.types config. If the factory only supports the DBAL 2 signature, shim it in a thin subclass rather than patching vendor code.src/DBAL/ of your app is a legitimate, low-risk option and removes the dependency entirely.null, [], and {} round-trip to what you expect. json_encode flags matter: without JSON_PRESERVE_ZERO_FRACTION, a 1.0 comes back as 1 and your float typed property silently becomes int.DateTime format is baked into every stored row. Configure the handler explicitly and never change it without a backfill.max_allowed_packet or a TEXT column's 64 KB. Log payload sizes once in staging.convertToDatabaseValue/convertToPHPValue and getSQLDeclaration are the only three methods that matter. Subclass for custom serialization context (groups, serializeNull) or a different SQL declaration — that subclass is usually the cleanest fix for every quirk above.How can I help you explore Laravel packages today?