orm icon indicating copy to clipboard operation
orm copied to clipboard

Allow to create Custom Type with metadata accessible from convertToPHPValue

Open nfacciolo opened this issue 3 months ago • 5 comments

Feature Request

What

Allow Custom Types to access field mapping metadata (like options or a new class parameter) in convertToPHPValue() and convertToDatabaseValue().

Example usage:

#[ORM\Column(type: 'string_vo', class: Firstname::class)]
private Firstname $firstName;

#[ORM\Column(type: 'string_vo', class: Email::class)]
private Email $email;

Why

Currently, creating Value Objects requires one Custom Type per class, even when the conversion logic is identical (string ↔ VO with __toString()).

The enumType parameter solves this elegantly for enums, but there's no equivalent for custom objects.

This leads to:

  • Boilerplate: dozens of nearly identical Type classes
  • Configuration overhead: one YAML entry per VO
  • Maintenance burden: any change must be replicated across all types

A generic approach would allow a single StringValueObjectType to handle all string-based Value Objects, similar to how enumType works.

How

Pass field mapping metadata to Type methods:

public function convertToPHPValue(
    mixed $value, 
    AbstractPlatform $platform,
    array $fieldMapping = []  // New optional parameter
): mixed {
    $class = $fieldMapping['class'] ?? null;
    if ($class && $value !== null) {
        return new $class($value);
    }
    return $value;
}

Alternatively, introduce a dedicated parameter like valueObjectClass similar to enumType.

This would maintain backward compatibility while enabling generic Type implementations for Value Objects.

nfacciolo avatar Nov 19 '25 10:11 nfacciolo

Not sure if the DBAL type system is a good fit for this feature. Maybe we should implement this in the ORM instead, like we did with enumType.

derrabus avatar Nov 23 '25 15:11 derrabus

Thank you for the feedback! I understand your point about the DBAL type system potentially not being the right fit. I mentioned enumType precisely because I see it as a precedent for this kind of flexibility - if implementing this at the ORM level (similar to enumType) would be the better architectural approach, I'm completely open to that solution. My core need is straightforward: I want to avoid creating multiple Type classes with identical conversion logic, differing only in the target Value Object class. Whether this is achieved through:

Field mapping metadata passed to DBAL Type methods, or An ORM-level implementation like enumType

...either approach would solve my use case. If the ORM layer is indeed the more appropriate place for this feature, I'd be happy to have the issue transferred there or to open a new one in the ORM repository. Just let me know what works best for the project architecture.

nfacciolo avatar Nov 24 '25 21:11 nfacciolo

If the ORM layer is indeed the more appropriate place for this feature, I'd be happy to have the issue transferred there

Done.

derrabus avatar Nov 29 '25 21:11 derrabus

Why not use embeddebales?

beberlei avatar Nov 30 '25 18:11 beberlei

Thanks for the suggestion! Embeddables are great for certain use cases, but they don't fit well here for a few reasons:

  1. Loss of flexibility for database constraints With embeddables, the Doctrine attributes (#[Column]) must be defined inside the Value Object class itself:
class Email {
    #[Column(type: 'string', unique: true)] // ⚠️ Fixed for ALL uses
    public function __construct(private string $value) {}
}

This creates a major problem: sometimes email must be unique, sometimes not, depending on the entity context. With embeddables, you're forced to choose one behavior for all cases, or create multiple Value Object classes for the same logical type. With a custom Type, each entity controls its own constraints:

// In User entity
#[Column(type: 'string_vo', unique: true, class: 'Email::class')]
private Email $email;

// In ContactForm entity  
#[Column(type: 'string_vo', unique: false, class: 'Email::class')]
private Email $secondEmail;
  1. Mapping boilerplate With embeddables, I need to explicitly map every property and each property:
#[Embedded(class: Email::class)]
private Email $email;

With a custom Type, the mapping is simpler and the same for each vo:

#[Column(type: 'string_vo', class: 'Name::class')]
private Name $name;


#[Column(type: 'string_vo', class: 'Email::class')]
private Email $email;
  1. Value Objects should stay persistence-agnostic Value Objects are domain concepts that shouldn't know about database concerns. With embeddables, you're forced to pollute your Value Objects with Doctrine-specific attributes:
use Doctrine\ORM\Mapping as ORM;

class Email {
    #[ORM\Column(type: 'string', length: 255)] // ❌ Domain layer knows about persistence
    public function __construct(private string $value) {
        // validation logic
    }
}

This creates tight coupling between the domain layer and the persistence layer. With custom Types, the Value Object stays clean:

  1. My actual need

I have dozens of single-value Value Objects that each need:

  • Identical conversion logic (validate → instantiate)
  • Different target classes

Currently, I must create one Type class per Value Object, duplicating the same conversion code. I'm looking for a way to have one reusable Type that can handle multiple Value Object classes based on field metadata - similar to how enumType works. Embeddables solve a different problem (multi-property value objects), not the "avoid duplicating Type classes" problem.

nfacciolo avatar Nov 30 '25 22:11 nfacciolo