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.