Parameters for value conversion in Doctrine types #7280

Open
opened 2026-01-22 15:48:52 +01:00 by admin · 8 comments
Owner

Originally created by @janopae on GitHub (Dec 14, 2023).

Feature Request

Q A
New Feature yes
RFC yes/no (what does this mean? this certainly is a request for comments, but it doesn't follow any formal RFC-related procedure)
BC Break no

Summary

While there are general options that you can provide to the getSQLDeclaration part of a Doctrine type, you can't provide any parameters to the conversion of values (from and to database values). The only way of parameterising the data conversion is subclassing the doctrine type.

This sometimes leads to a lot of Doctrine type subclasses for very specific cases, which all have to be registered, both in production code and in tests. For parameters like number or string values which are specific for one entity, I hope you understand that subclassing really doesn't feel like the right approach.

This could be solved by passing parameters to the conversion methods. One rather simple way of doing this would be passing the options as a third parameter to convertToDatabaseValue and convertToPHPValue. As additional parameters passed to a PHP function are just ignored, this wouldn't even be a BC break.

Example

Consider the following type, which wraps entity IDs in value objects:

<?php

namespace TimeTrackingBundle\ValueObjectAlsEntityId;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type;

/**
 * @template T of EntityId
 */
abstract class IdValueObjectType extends Type
{
    /**
     * @return class-string<T>
     */
    abstract protected function getValueObjectClass(): string;

    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        return $platform->getIntegerTypeDeclarationSQL($column);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int
    {
        if (null === $value) {
            return null;
        }

        if (is_numeric($value)) {
            return (int) $value;
        }

        $classString = $this->getValueObjectClass();

        if (!$value instanceof $classString) {
            throw new \RuntimeException('Got '.get_debug_type($value));
        }

        return $value->toInt();
    }

    /**
     * @return ?T
     */
    public function convertToPHPValue(mixed $value, AbstractPlatform $platform)
    {
        if (null === $value) {
            return null;
        }

        if (!is_numeric($value)) {
            throw new \RuntimeException('Got '.get_debug_type($value));
        }

        return $this->getValueObjectClass()::fromInt((int) $value);
    }

    public function getBindingType(): int
    {
        return ParameterType::INTEGER;
    }

    public function requiresSQLCommentHint(AbstractPlatform $platform): bool
    {
        return true;
    }
}

If the reason you want to use Value Objects is that you want to prevent confusing ID values of different entities with one another, you need to subclass this for every entity, so each entity can have its own Value Object class.

If convertToDatabaseValue and convertToPHPValue got a third options parameter, this wouldn't be necessary – instead, you could just use it as follows:

class MyEntity {
    #[ORM\Id]
    #[ORM\Column(type: 'entity_id', options: ['valueObjectClass' => MyEntityId::class, 'unique' => true])]
    private MyEntityId $id;
}

You could pretty much use parameters for any type of value object which can be created in multiple specific ways.

Some other general examples could be a MoneyType, which has a currency parameter (if all database values of an entity are saved in the same currency) or a LimitedHtmlType, which gets hydrated to a value object that escapes all HTML tags except a small list of allowed ones.

Originally created by @janopae on GitHub (Dec 14, 2023). ### Feature Request <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | New Feature | yes | RFC | yes/no (what does this mean? this certainly is a request for comments, but it doesn't follow any formal RFC-related procedure) | BC Break | no #### Summary While there are general options that you can provide to the `getSQLDeclaration` part of a Doctrine type, you can't provide any parameters to the conversion of values (from and to database values). The only way of parameterising the data conversion is subclassing the doctrine type. This sometimes leads to a lot of Doctrine type subclasses for very specific cases, which all have to be registered, both in production code and in tests. For parameters like number or string values which are specific for one entity, I hope you understand that subclassing really doesn't feel like the right approach. This could be solved by passing parameters to the conversion methods. One rather simple way of doing this would be passing the options as a third parameter to `convertToDatabaseValue` and `convertToPHPValue`. As additional parameters passed to a PHP function are just ignored, this wouldn't even be a BC break. #### Example Consider the following type, which wraps entity IDs in value objects: ```php <?php namespace TimeTrackingBundle\ValueObjectAlsEntityId; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; /** * @template T of EntityId */ abstract class IdValueObjectType extends Type { /** * @return class-string<T> */ abstract protected function getValueObjectClass(): string; public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { return $platform->getIntegerTypeDeclarationSQL($column); } public function convertToDatabaseValue($value, AbstractPlatform $platform): ?int { if (null === $value) { return null; } if (is_numeric($value)) { return (int) $value; } $classString = $this->getValueObjectClass(); if (!$value instanceof $classString) { throw new \RuntimeException('Got '.get_debug_type($value)); } return $value->toInt(); } /** * @return ?T */ public function convertToPHPValue(mixed $value, AbstractPlatform $platform) { if (null === $value) { return null; } if (!is_numeric($value)) { throw new \RuntimeException('Got '.get_debug_type($value)); } return $this->getValueObjectClass()::fromInt((int) $value); } public function getBindingType(): int { return ParameterType::INTEGER; } public function requiresSQLCommentHint(AbstractPlatform $platform): bool { return true; } } ``` If the reason you want to use Value Objects is that you want to prevent confusing ID values of different entities with one another, you need to subclass this for every entity, so each entity can have its own Value Object class. If `convertToDatabaseValue` and `convertToPHPValue` got a third options parameter, this wouldn't be necessary – instead, you could just use it as follows: ```php class MyEntity { #[ORM\Id] #[ORM\Column(type: 'entity_id', options: ['valueObjectClass' => MyEntityId::class, 'unique' => true])] private MyEntityId $id; } ``` You could pretty much use parameters for any type of value object which can be created in multiple specific ways. Some other general examples could be a `MoneyType`, which has a `currency` parameter (if all database values of an entity are saved in the same currency) or a `LimitedHtmlType`, which gets hydrated to a value object that escapes all HTML tags except a small list of allowed ones.
Author
Owner

@mpdude commented on GitHub (Jan 15, 2024):

Types are a concept of doctrine/dbal, not doctrine/orm. I am not so familiar with that, so take my explanations with a grain of salt. Maybe @morozov can amend or correct me.

From the DBAL point of view, you bind values to query parameters, optionally specifiying a type. That means you'd need to provide the "general options" you're referring to at the time you're binding values to parameters. This is probably not what you want. I don't see how DBAL could otherwise get hold of the right type "extra parameters" to use. This is probably why, on the DBAL side of things, Types are designed in this static way, without parameterization.

When the ORM builds queries based on DQL expressions, it knows what entity classes and fields you refer to, and it can look up @Column definitions in its class metadata. But there is no way to pass it on to DBAL type bindings.

@mpdude commented on GitHub (Jan 15, 2024): `Type`s are a concept of doctrine/dbal, not doctrine/orm. I am not so familiar with that, so take my explanations with a grain of salt. Maybe @morozov can amend or correct me. From the DBAL point of view, you bind values to query parameters, optionally specifiying a type. That means you'd need to provide the "general options" you're referring to at the time you're binding values to parameters. This is probably not what you want. I don't see how DBAL could otherwise get hold of the right type "extra parameters" to use. This is probably why, on the DBAL side of things, `Type`s are designed in this static way, without parameterization. When the _ORM_ builds queries based on DQL expressions, it knows what entity classes and fields you refer to, and it can look up `@Column` definitions in its class metadata. But there is no way to pass it on to DBAL type bindings.
Author
Owner

@antman3351 commented on GitHub (Jan 15, 2024):

Even just having options for convertToPHPValue would be a great improvement would that be possible? I could only find it being used by Connection::convertToPHPValue in the dbal folder

@antman3351 commented on GitHub (Jan 15, 2024): Even just having options for `convertToPHPValue` would be a great improvement would that be possible? I could only find it being used by `Connection::convertToPHPValue` in the dbal folder
Author
Owner

@janopae commented on GitHub (Jan 15, 2024):

From the DBAL point of view, you bind values to query parameters, optionally specifiying a type. That means you'd need to provide the "general options" you're referring to at the time you're binding values to parameters. This is probably not what you want.

Why not?

If you have to repeat type declarations for every query, that doesn't get worse with paremeterised types. Repeatedly writing setParameter($car, IdType::class, [Car::class]) when binding query parameters is not much worse than repeatedly writing setParameter($car, CarIdType::class);.

EDIT: Updated pseudo-syntax to something more realistic.

@janopae commented on GitHub (Jan 15, 2024): > From the DBAL point of view, you bind values to query parameters, optionally specifiying a type. That means you'd need to provide the "general options" you're referring to at the time you're binding values to parameters. This is probably not what you want. Why not? If you have to repeat type declarations for every query, that doesn't get worse with paremeterised types. Repeatedly writing `setParameter($car, IdType::class, [Car::class])` when binding query parameters is not much worse than repeatedly writing `setParameter($car, CarIdType::class);`. EDIT: Updated pseudo-syntax to something more realistic.
Author
Owner

@greg0ire commented on GitHub (Jan 15, 2024):

We plan to release DBAL 4 soon, and I think this might help: https://github.com/doctrine/dbal/pull/5036

@greg0ire commented on GitHub (Jan 15, 2024): We plan to release DBAL 4 soon, and I think this might help: https://github.com/doctrine/dbal/pull/5036
Author
Owner

@mpdude commented on GitHub (Jan 15, 2024):

How/when does DBAL use types to convert database to PHP values? Do you need to provide the types for columns when iterating result arrays/rows?

@mpdude commented on GitHub (Jan 15, 2024): How/when does DBAL use types to convert database to PHP values? Do you need to provide the types for columns when iterating result arrays/rows?
Author
Owner

@mpdude commented on GitHub (Jan 15, 2024):

@greg0ire I may be mistaken, but the PR you referenced allows registering different instances of the same type under different names? That allows passing parameters to the type constructor.

I think what was requested here is a tad more dynamic, with the parameters being options declared on the ORM fields where the type is used.

@mpdude commented on GitHub (Jan 15, 2024): @greg0ire I may be mistaken, but the PR you referenced allows registering different instances of the same type under different names? That allows passing parameters to the type constructor. I think what was requested here is a tad more dynamic, with the parameters being options declared on the ORM fields where the type is used.
Author
Owner

@greg0ire commented on GitHub (Jan 15, 2024):

Yes, it would only be a workaround as it only addresses this point:

The only way of parameterising the data conversion is subclassing the doctrine type.

@greg0ire commented on GitHub (Jan 15, 2024): Yes, it would only be a workaround as it only addresses this point: > The only way of parameterising the data conversion is subclassing the doctrine type.
Author
Owner

@antman3351 commented on GitHub (Jan 24, 2024):

How/when does DBAL use types to convert database to PHP values? Do you need to provide the types for columns when iterating result arrays/rows?

To go any further with the discussion this needs to be answered.

I never use DBAL on it's own, I always use the ORM so I probably doing it wrong, but I couldn't get the DBAL to convert the built in types for a select query 🤔

created_at is type datetime, but in the result it's a stirng ( using MariaDB )

$conn = \container( EntityManager::class )->getConnection();
$sql = "SELECT id, created_at FROM customer WHERE id_customer = 2";
$stmt = $conn->prepare($sql);
$resultSet = $stmt->executeQuery();
var_dump( $resultSet->fetchAllAssociative() );
@antman3351 commented on GitHub (Jan 24, 2024): > How/when does DBAL use types to convert database to PHP values? Do you need to provide the types for columns when iterating result arrays/rows? To go any further with the discussion this needs to be answered. I never use DBAL on it's own, I always use the ORM so I probably doing it wrong, but I couldn't get the DBAL to convert the built in types for a select query 🤔 created_at is type datetime, but in the result it's a stirng ( using MariaDB ) ```PHP $conn = \container( EntityManager::class )->getConnection(); $sql = "SELECT id, created_at FROM customer WHERE id_customer = 2"; $stmt = $conn->prepare($sql); $resultSet = $stmt->executeQuery(); var_dump( $resultSet->fetchAllAssociative() ); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#7280