03/24/2015

Doctrine field type mappings: tinyint is a boolean

When Doctrine reads an existing MySQL schema, a tinyint column maps to Doctrine's boolean regardless of the declared length. TINYINT(1), TINYINT(4), a column holding 0 to 100 -- all of them are described as boolean. Generate an entity from that schema and the field is a boolean, so a stored 50 comes back as true.

That is DBAL's own platform mapping, applied when it introspects the database (schema diffs and reverse engineering), not a bug, and it is still the behavior in DBAL 4.5. A field you map yourself as smallint is not converted: it comes back as an integer. The surprise is that TINYINT(4) looks like a number and is read as a flag.

So how do you store a small integer?

Two ways, and the first is almost always the right one.

Use smallint and let the column be SMALLINT:

#[ORM\Column(type: Types::SMALLINT)]
private int $rating;

One byte more per row than TINYINT, and everything works: schema tools, migrations, and other applications reading the same table.

Or keep TINYINT and say so explicitly:

#[ORM\Column(type: Types::SMALLINT, columnDefinition: 'TINYINT NOT NULL')]
private int $rating;

columnDefinition writes that SQL verbatim, so the column stays TINYINT while PHP sees an integer. The cost is real: Doctrine can no longer compare the column to its own idea of what the type should be, so schema diffing gets less useful and migrations need more care. Reach for it when the schema is not yours to change.

The full mapping

MySQL column type on the left, the Doctrine type DBAL assigns when it reads that column. Checked against DBAL 4.5.0; the differences from 3.9 are noted below the table.

MySQL Doctrine
tinyint boolean
smallint smallint
mediumint, int, integer integer
bigint bigint
tinytext, mediumtext, longtext, text text
varchar, string, char string
date date
datetime, timestamp datetime
time time
float smallfloat (DBAL 4.1 and later; float before)
double, real float
decimal, numeric decimal
year date
blob, tinyblob, mediumblob, longblob blob
binary, varbinary binary
set simple_array
json json
enum enum (DBAL 4.2 and later)

What PHP gets back differs by type. decimal and numeric come back as a PHP string, to keep precision. bigint comes back as an int when the value fits and as a string when it does not (DBAL 4); in DBAL 3.9 it is always a string. blob is a stream resource in both 3.9 and 4. binary is a stream resource in 3.9 and a string in 4.

Three of these are worth a second look.

Every kind of text is text. The distinction between TINYTEXT and LONGTEXT — which is a real difference in maximum length — disappears at the mapping layer. Doctrine will happily let you write more than a TINYTEXT can hold and MySQL will truncate it, silently or not depending on strict mode.

timestamp and datetime both become datetime, so the automatic updating behavior a TIMESTAMP column may have in MySQL is invisible to Doctrine. If the database is updating that column behind you, refresh the entity or Doctrine will hand back the value it last knew.

set becomes simple_array. It works, and simple_array is comma-joined text with no escaping, so a value containing a comma breaks it. MySQL's SET has its own constraints that Doctrine knows nothing about.

Where the source of truth lives

This table comes from initializeDoctrineTypeMappings() in DBAL's AbstractMySQLPlatform (checked at tag 4.5.0). If you need to check a type not listed here, that method is the answer rather than any article — including this one. It has moved from lib/ to src/, and from MySqlPlatform (DBAL 2.13) to MySQLPlatform (3.0) to AbstractMySQLPlatform (3.9 and 4), which is why old links into the file no longer resolve. The types reference is the stable address.

Questions this keeps raising

Why does my TINYINT column return true instead of a number?

Because DBAL's MySQL platform maps tinyint to boolean regardless of the declared length when it reads an existing schema, so an entity generated from it has a boolean field and TINYINT(4) holding 7 comes back as true. Map the field as smallint, or keep the column and set columnDefinition to TINYINT so PHP sees an integer.

Does TINYINT(1) versus TINYINT(4) change anything?

Not to Doctrine. The display width in parentheses has never affected the range MySQL stores, and DBAL maps every tinyint the same way. MySQL 8.0.17 deprecated the display width for integer types entirely.

Is it safe to use columnDefinition?

It works and it costs you something. Doctrine writes your SQL verbatim and can no longer reason about the column, so schema comparison and migration diffing become less reliable for that field. Use it when the schema belongs to somebody else; prefer smallint when it is yours.

Why is my long text being truncated?

Because tinytext, mediumtext, longtext and text all map to Doctrine's text, so the mapping layer cannot tell you the column is a TINYTEXT holding 255 bytes. Doctrine will send more than fits and MySQL decides whether to truncate or error depending on strict mode.