Skip to content
Advertisement

How to get column properties in Doctrine custom type convertToDatabaseValue function?

As the title suggests, I am making my own Type in Doctrine and the type has its precision and scale options in getSQLDeclaration() function. I need somehow to access these from convertToDatabaseValue() as well, as I need to round the number with given precision.

<?php

namespace AppDBALTypes;

use DecimalDecimal;
use DoctrineDBALPlatformsAbstractPlatform;
use DoctrineDBALTypesDecimalType as DoctrineDecimalType;

class DecimalType extends DoctrineDecimalType
{
    public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
    {
        $column['precision'] = empty($column['precision'])
            ? 10 : $column['precision'];
        $column['scale']     = empty($column['scale'])
            ? 0 : $column['scale'];

        return 'DECIMAL(' . $column['precision'] . ', ' . $column['scale'] . ')' .
            (!empty($column['unsigned']) ? ' UNSIGNED' : '');
    }

    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        if (is_null($value)) {
            return null;
        }

        return new Decimal($value);
    }

    public function convertToDatabaseValue($value, AbstractPlatform $platform)
    {
        if ($value instanceof Decimal) {
            // HERE!! This is the line where I need to specify precision based on column declaration
            return $value->toFixed(????);
        }

        return parent::convertToDatabaseValue($value, $platform); // TODO: Change the autogenerated stub
    }
}

So the Entity column then looks like:

    #[ORMColumn(type: 'decimal', precision: 10, scale: 4)]
    private Decimal $subtotal;

And I need to get the scale or precision part in the convertToDatabaseValue() function.

Advertisement

Answer

As mentioned in my comments, what you’re attempting to do is not supported. While there are methods of forcing a similar type of functionality, such as with lifecycle callbacks, they are considered extremely bad-practice as they point towards persistence issues.
The general principal is that Entities should work without relying on either the ORM or the Database layers, and implementing workarounds to force the behavior may result in breaks in your application.

In practice the value being supplied to the Entity should be formatted as desired based on the schema rules, exactly as one would when supplying values to a native SQL query.
This ensures the integrity and consistency of the Entity data is maintained throughout the application and the underlying data that each Entity holds is always valid, without requiring the ORM or database layers to “fix” the data.

$decimal = new Decimal('1337.987654321');
$entity = new Entity();
$entity->setSubtotal($decimal->toFixed(4));

echo $entity->getSubtotal(); // 1337.9876

In contrast to the example above, when relying on the ORM or database layer, the data would be invalid until after $em->flush(); was called to “fix” the value format based on the ORM specifications and calling convertToDatabaseValue(). Whereby calling $entity->getSubtotal(); prior to $em->flush() would instead return 1337.987654321 which does not conform to the column specifications, causing business logic relying on the value to become broken.

Object Casting Example

Since you are relying on a separate library package that is not suitable to modify or to switch to a custom object.
Another approach is to utilize the default Doctrine decimal type specifications which works on string data-type values and utilizing casting within the Entity instead. Circumventing the need for a custom DecimalType or workarounds and the entity would more easily be applicable to the rest of the Symfony framework without the need for additional workarounds.

3V4L Example

class Entity
{
    #[ORMColumn(type: 'decimal', precision: 10, scale: 4)]
    private string $subtotal = '0.0';

    // name as desired
    public function getSubtotalDecimal(): Decimal
    {
        return new Decimal($this->subtotal);
    }

    public function getSubtotal(): string
    {
        return $this->subtotal;
    }

    // see Union Types
    public function setSubtotal(string|Decimal $subtotal): self
    {
        if ($subtotal instanceof Decimal) {
            $subtotal = $subtotal->toFixed(4);
        }
        $this->subtotal = $subtotal;

        return $this;
    }
}
$entity = new Entity();
$entity->setSubtotal(new Decimal('1337.987654321'));

echo $entity->getSubtotal(); // 1337.9876
echo $entity->getSubtotalDecimal()->toFixed(4); // 1337.9876

It is recommended to use a DTO (data-transfer object) to perform validation and formatting of the data prior to being applied to the Entity (see DTO Example below).


DTO Example

Basic usage of Symfony generally allows for manipulation of the entities directly, such as when using Forms and EntityType fields. However, with the principal that the Entities are always valid, there should not be a need to validate the Entity data that was injected into it by the Form containing user-supplied data.

To prevent invalid data within the Entity and promote separations of concerns, a DTO can be used to structure the data based on the immediate concerns for what that data is being used for.

To make DTOs easier to utilize, the Doctrine ORM supports the generation of DTOs, which can be used as the model in Forms.
It is also important to note that Doctrine has deprecated partial objects support in the future in favor of using a DTO for partial objects [sic].

class OrderSubtotalDTO
{
    private string $company;
    private string $subtotal;

    public function __construct(string $company, string $subtotal)
    {
        $this->company = $company;
        $this->subtotal = $subtotal;
    }

     // ...

    public function setSubtotal(string $value): self
    {
        $this->subtotal = $value;

        return $this;
    }

    public function apply(Entity $entity): self
    {
        // apply the formatting being supplied to the entity
        $entity->setSubtotal(new Decimal($this->subtotal)->toFixed(4));

        return $this;
    }
}
// use a Query service to reduce code duplication - this is for demonstration only
$q = $em->createQuery('SELECT NEW OrderSubtotalDTO(e.company, e.subtotal) FROM Entity e WHERE e.id = ?1');
$q->setParameter(1, $entity->getId());
$orderSubtotalDTO = $q->getOneOrNull();

$form = $formFactory->create(OrderSubtotalForm::class, $orderSubtotalDTO);
$form->handleRequest($request);

if ($form->isSubmitted() && $form->isValid()) {
    $orderSubtotalDTO->apply($entity);
    $em->flush();
}
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement