03/24/2015

Doctrine field type mappings: tinyint is a boolean

A MySQL tinyint maps to Doctrine's boolean regardless of the declared length. TINYINT(1), TINYINT(4), a column holding 0 to 100 -- all of them come back from the database as true or false.

That is DBAL's own platform mapping, not a bug, and it is still the behavior in DBAL 4. It surprises people because TINYINT(4) looks like a number and behaves like 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 it becomes on the right.

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, double, real float
decimal, numeric decimal
year date
blob, tinyblob, mediumblob, longblob blob
binary, varbinary blob
set simple_array

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 the MySQL platform class in DBAL. If you need to check a type not listed here, that class is the answer rather than any article — including this one. It moved from lib/ to src/ and from MySqlPlatform to MySQLPlatform between DBAL 2 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, so TINYINT(4) holding 7 comes back as true. Use smallint for the field, 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.