Doctrine converts a value only when it knows its type.
A mapped field such as p.tags is converted by its DBAL type. A text[] column arrives as a PHP array.
Anything the query computes - a function, an aggregate, arithmetic - is not converted. Doctrine does not know what type it returns, so you get the value exactly as the PostgreSQL driver hands it over, usually a string.
SELECT p.tags, ARRAY_APPEND(p.tags, 'new') AS extended FROM App\Entity\Product p WHERE p.id = 1
getScalarResult(), getSingleColumnResult() and getSingleScalarResult() convert nothing, not even a mapped field: p.tags arrives as '{php,postgres}' and p.price as '42.00'. Use getResult(), getSingleResult() or getOneOrNullResult() when you want mapped fields converted.
The driver converts a few types by itself, even for computed values: boolean becomes bool; smallint, integer and bigint become int; real and double precision become floatfrom PHP 8.4 (numeric strings before).
The values on this page were measured on PostgreSQL 18 and PostGIS 3.6 with PHP 8.5, DBAL 4.4 and ORM 3.7, with the session time zone set to UTC.
Converting a computed value yourself
If a computed value has the same PostgreSQL type as one of your mapped columns, pass it through that DBAL type:
SELECT ARRAY_APPEND(p.tags, 'new') AS tags FROM App\Entity\Product p WHERE p.id = 1
Selecting both gives a mixed row: the entity under key 0 and each computed value under its alias.
SELECT s, OVER(SUM(s.amount), PARTITION BY s.customer ORDER BY s.placedAt) AS runningTotal FROM App\Entity\Sale s ORDER BY s.id
[0 => Sale {id: 1, …}, 'runningTotal' => '42.00']
[0 => Sale {id: 2, …}, 'runningTotal' => '51.99']
DTOs with SELECT NEW
SELECT NEW follows the same rule: a mapped field arrives converted, a computed value arrives as the driver returns it. Type each constructor parameter by what it receives:
SELECT NEW App\Dto\CustomerTotal(c.name, SUM(s.amount)) FROM App\Entity\Sale s JOIN s.customer c GROUP BY c.id, c.name
finalclassCustomerTotal{publicfunction__construct(publicreadonlystring$name,publicreadonlystring$total,// SUM of a numeric column arrives as '51.99', not as a float){}}
What a mapped column arrives as
What an entity property holds, and what a mapped field holds in a getResult() or getArrayResult() row. Available types lists the DBAL type for each column type.
Column type
PHP value
Good to know
text[], varchar[], citext[]
list<?string>
Items stay strings: {php,1.0,true} reads as ['php', '1.0', 'true']
A jsonb column returns whatever JSON it holds - not always an array. If a row can hold a plain number or string, type the property mixed (or array|int|float|string|bool|null), or PHP throws Cannot assign int to property … of type ?array.
An empty bytea, bit varying or xml value reads back as null, the same as SQL NULL. An empty hstore reads back as [] and an empty ltree as an Ltree with no labels.
bytea and bytea[] need bytea_output at its default, hex. With escape, bytea[] cannot be read, and neither can bytea through DBAL’s pgsql driver, which decodes only the hex format.
Time zone and interval style
Dates and intervals that arrive as strings are formatted by the connection’s session settings, so the same query can return different text on two servers. TimeZone changes every timestamptz string, and IntervalStyle changes every interval string. With TimeZone = 'Europe/Sofia' and IntervalStyle = 'iso_8601', AGE(s.placedAt, '2026-01-01') returns 'P8M25DT13H30M' instead of '8 mons 25 days 10:30:00' - a different answer, not just a different format, because the day starts at a different instant.
If you read these strings, set the time zone when you connect (SET TIME ZONE 'UTC'). Mapped date and range fields are safe: they keep their offset, so they compare correctly under any zone. The Interval type reads every IntervalStyle, so convertToPHPValue($row['age'], 'interval') works on any server.
Errors
getSingleScalarResult() returns '{php,postgres}' for p.tags. The scalar result methods skip the DBAL type. Use getSingleResult() and take the column from the row.
Cannot assign int to property App\Entity\Product::$attributes of type ?array. That row’s jsonb value is a plain number. Type the property mixed; see jsonb.
Type "jsonb" already exists, or a big jsonb integer arrives as a float. DBAL 4.3+ ships its own jsonb type. Register this library’s type with Type::overrideType('jsonb', Jsonb::class).