Skip to main content

Converters

Every mapped property has a converter. It turns the PHP value into something the database can bind, and turns what the driver reports back into the property's type. That is what makes a bool survive SQLite reporting 1 and PostgreSQL reporting true.

What ships

KeyProperty typeWritesReads
stringstringthe stringcasts whatever the driver reports to string
intintthe integeran int, or a numeric string
floatfloatthe float, or a numeric stringa float, or a numeric string
boolboolthe booleantrue, 1, '1', false, 0 or '0'
jsonarrayjson_encodejson_decode to an array
serializedarrayserializeunserialize, with classes disallowed

The read side is lenient because drivers differ about what they report for the same column. The write side is strict, because the property's own type already guarantees what it holds; float is the one exception, taking a numeric string as well. A value a converter cannot make sense of throws TypeConversionException rather than being coerced.

caution

json and serialized are not chosen for you. A bare array property has no converter registered under array, so it throws TypeConversionException until you name one with #[Column(converter: 'json')]. Which representation belongs in the column is not something the package can guess.

The serialized converter passes allowed_classes: false, so a payload can only ever come back as arrays and scalars. Unserializing objects from a column is how a database row turns into code execution, and no option is offered to turn it on.

How one is chosen

  1. The converter option on #[Id] or #[Column], if given.
  2. Otherwise the property's type name: string, int, float, bool, or a class name.

The name is looked up in the TypeRegistry. With nothing registered under it, the registry tries one last thing: a name that is a backed enum gets a converter built for it. Anything else throws TypeConversionException naming the type, which is what a DateTimeImmutable property does out of the box.

#[Column(converter: 'json')]
public array $meta; // the named converter

public bool $published; // looked up as 'bool'

Null

A converter never sees null. The package handles it around them:

SituationResult
Column is NULL, property is nullableThe property is set to null.
Column is NULL, property is not nullableHydrationException, naming the property and column.
Property is null, property is nullableNULL is written.
Property is null, property is not nullablePersistenceException, naming the property.

So a converter you write only ever receives a value, and never has to answer for null.

Backed enums

A backed enum needs no registration. Ask the registry for a type it does not know, and if that type is a backed enum it builds a BackedEnumConverter for it and keeps it.

enum Role: string
{
case Admin = 'admin';
case Member = 'member';
}
public Role $role;

The column stores the backing value, 'admin', and reads back as Role::Admin. One converter is built per enum, the first time that enum is mapped.

A backing value the column holds that the enum does not have throws TypeConversionException, with the original ValueError attached.

caution

An enum with no backing type cannot be mapped. There is no value a column could hold for enum Role { case Admin; }, so it throws TypeConversionException naming the enum, and it throws while mapping the class rather than when a row is read — so it fails on the first of() call.

To map an enum some other way, register a converter under its class name and that one is used instead:

$types = new TypeRegistry([new LabelledRoleConverter()]);

The registry

$types = new TypeRegistry();

$types->register(new MoneyConverter());

$types->has('money'); // bool
$types->get('money'); // TypeConverter, or TypeConversionException
MethodReturnsNotes
register(TypeConverter $converter)voidKeyed by the converter's own type().
has(string $type)boolTrue for a registered type and for any backed enum.
get(string $type)TypeConverterBuilds one for a backed enum; throws for anything else unregistered.
resolve(string $propertyType, ?string $converterType = null)TypeConverterThe named converter, falling back to the property type.

The constructor registers the built-in converters first and the ones you pass after, so registering under a built-in key replaces it. That is the way to change how every bool in the application is written without touching an entity.

$types = new TypeRegistry([new StrictBooleanConverter()]);

Next: Writing a converter.