Lookup/Enum table annotation #6024

Closed
opened 2026-01-22 15:24:56 +01:00 by admin · 4 comments
Owner

Originally created by @xxRockOnxx on GitHub (Jul 21, 2018).

Originally assigned to: @Ocramius on GitHub.

Feature Request

Q A
New Feature yes
RFC not sure
BC Break not sure

Summary

There are times where fetching is really unnecessary. Good example are "Enum tables" or "Lookup table"

Imagine this flow:

Request -> Controller -> Service -> Repo

In your request you might have something like

['contacts' => ['type' => 'email', 'value' => 'sample@test.com']]

Where would it fit?

  • My controller transforms those arrays into entities to be passed on service.
  • My service layer is just business logic. It'll be really a pain to do something like ContactTypeRepo::find() especially when you have deep object.
  • My repo layer does CRUD operations only and no logic

Wouldn't it be a lot easier if we have an entity annotation that look like

class Contact
{
    /**
    * @ORM\ManyToOne(targetEntity="ContactType", lookup="true")
    */
    private $type;
}

You can then set it either the id or the entity itself (without coming from entityManager. Just:

$type = new ContactType(1, 'email'); // Manual entity creation instead of ContactTypeRepo::find(1)
$contact->setType($type);

then EntityManager will automatically get the reference for it? Because right now if you do that, the error would be

A new entity was found through the relationship 'App\Models\Contact#type'

Workaround

class Contact
{
    /**
    *  @ORM\Column(type="integer", name="type_id")
    */
    private $type;
}
// Called in controller before sending response
class ContactTransformer
{
    public function transform(Contact $contact)
    {
        if (is_int($contact->getType())) {
            $type = ContactType::getTypeInstance($contact->getType());
        } else {
            $type = $contact->getType();
        }

        return [
            'type' => (new TypeTransformer())->trasnform($type),
            'value' => $contact->getValue()
        ];
    }
}
class ContactType
{
    public const EMAIL = 1;

    public static function getTypeInstance(int $type)
    {
        $names = [
            static::EMAIL => 'Email address'
        ];

        return new static($type, $names[$type]);
    }
}

Thoughts?

Originally created by @xxRockOnxx on GitHub (Jul 21, 2018). Originally assigned to: @Ocramius on GitHub. ### Feature Request | Q | A |------------ | ------ | New Feature | yes | RFC | not sure | BC Break | not sure #### Summary There are times where fetching is really unnecessary. Good example are "Enum tables" or "Lookup table" Imagine this flow: Request -> Controller -> Service -> Repo In your request you might have something like ``` ['contacts' => ['type' => 'email', 'value' => 'sample@test.com']] ``` Where would it fit? - My controller transforms those arrays into entities to be passed on service. - My service layer is just business logic. It'll be really a pain to do something like `ContactTypeRepo::find()` especially when you have deep object. - My repo layer does CRUD operations only and no logic Wouldn't it be a lot easier if we have an entity annotation that look like ``` class Contact { /** * @ORM\ManyToOne(targetEntity="ContactType", lookup="true") */ private $type; } ``` You can then set it either the id or the entity itself (without coming from entityManager. Just: ``` $type = new ContactType(1, 'email'); // Manual entity creation instead of ContactTypeRepo::find(1) $contact->setType($type); ``` then EntityManager will automatically get the reference for it? Because right now if you do that, the error would be > A new entity was found through the relationship 'App\Models\Contact#type' #### Workaround ``` class Contact { /** * @ORM\Column(type="integer", name="type_id") */ private $type; } ``` ``` // Called in controller before sending response class ContactTransformer { public function transform(Contact $contact) { if (is_int($contact->getType())) { $type = ContactType::getTypeInstance($contact->getType()); } else { $type = $contact->getType(); } return [ 'type' => (new TypeTransformer())->trasnform($type), 'value' => $contact->getValue() ]; } } ``` ``` class ContactType { public const EMAIL = 1; public static function getTypeInstance(int $type) { $names = [ static::EMAIL => 'Email address' ]; return new static($type, $names[$type]); } } ``` Thoughts?
admin added the New FeatureWon't FixQuestion labels 2026-01-22 15:24:56 +01:00
admin closed this issue 2026-01-22 15:24:56 +01:00
Author
Owner

@Ocramius commented on GitHub (Jul 23, 2018):

then EntityManager will automatically get the reference for it?

No, that won't happen. Doctrine only changes the state of your objects when:

  • refreshing (user triggered)
  • saving (if you use a post-insert id generator, such as AUTO_INCREMENT)
  • clearing (edge case, identifier is cleared)

The ORM shouldn't and won't modify any existing fields, and should be limited to persistence and serialisation from/to DB.

For everything that is in-memory, you are directly responsible for using the correct type.

In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative.

@Ocramius commented on GitHub (Jul 23, 2018): > then EntityManager will automatically get the reference for it? No, that won't happen. Doctrine only changes the state of your objects when: * refreshing (user triggered) * saving (if you use a post-insert id generator, such as `AUTO_INCREMENT`) * clearing (edge case, identifier is cleared) The ORM shouldn't and won't modify any existing fields, and should be limited to persistence and serialisation from/to DB. For everything that is in-memory, you are directly responsible for using the correct type. In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative.
Author
Owner

@xxRockOnxx commented on GitHub (Jul 23, 2018):

The ORM shouldn't and won't modify any existing fields, and should be limited to persistence and serialisation from/to DB.

Yes I agree. What I am asking is not even related to modification or any operations but rather automatically getting a Reference for an entity. As in:

instead of (Because in some cases, having ContactTypeRepo is really unncessary)

$type = ContactTypeRepo::find(1); 

$contact = new Contact();
$contact->setValue('test@test.com');
$contact->setType($type)

the annotation (what i am asking) would do:

/**
 * @Column(type="integer", lookup_type="App\Models\ContactType")
 */
private $type;

$contact = new Contact();
$contact->setType(1);
$contact->setValue('test@test.com')

// Annotation behind the scene will do
$this->em->getReference(ContactType::class, 1)

TL;DR: instead of fetching for an entity from entitymanager/repo, just allow setting the Identifier for the entity

I am open for clarifications if still not clear.

In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative.

Uhm, what? Another scenario for this might User - Role relationship. How can you ensure the integrity if you would only do ENUMS in application layer? That's how I understand what you said @Ocramius

@xxRockOnxx commented on GitHub (Jul 23, 2018): > The ORM shouldn't and won't modify any existing fields, and should be limited to persistence and serialisation from/to DB. Yes I agree. What I am asking is not even related to modification or any operations but rather automatically getting a `Reference` for an entity. As in: instead of (Because in some cases, having `ContactTypeRepo` is really unncessary) ``` $type = ContactTypeRepo::find(1); $contact = new Contact(); $contact->setValue('test@test.com'); $contact->setType($type) ``` the annotation (what i am asking) would do: ``` /** * @Column(type="integer", lookup_type="App\Models\ContactType") */ private $type; $contact = new Contact(); $contact->setType(1); $contact->setValue('test@test.com') // Annotation behind the scene will do $this->em->getReference(ContactType::class, 1) ``` *TL;DR*: instead of fetching for an entity from entitymanager/repo, just allow setting the Identifier for the entity I am open for clarifications if still not clear. > In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative. Uhm, what? Another scenario for this might `User - Role` relationship. How can you ensure the integrity if you would only do `ENUMS` in application layer? That's how I understand what you said @Ocramius
Author
Owner

@Ocramius commented on GitHub (Jul 23, 2018):

$type = ContactTypeRepo::find(1); 

Please don't do that in first place: it is overall a bad idea to rely on static global state for this sort of operation

the annotation (what i am asking) would do:

@Column(type="integer", lookup_type="App\Models\ContactType")

This is still incorrect: it's either an int or a App\Models\ContactType. If you want you can use a int|App\Models\ContactType inside your DBAL type (what you proposed in the original issue description), but this is clearly a bad practice.

TL;DR: instead of fetching for an entity from entitymanager/repo, just allow setting the Identifier for the entity

Use EntityManager#getReference() if you are already confident that the value exists, and you rely on foreign key integrity for the check: it converts a given identifier into a proxy or an existing entity of the requested type.

In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative.

Uhm, what? Another scenario for this might User - Role relationship. How can you ensure the integrity if you would only do ENUMS in application layer? That's how I understand what you said @Ocramius

One thing is value types, the other is having maps with arbitrary identifiers.

For value types, application-level constraints via value objects are more than sufficient:

final class Role
{
    private const ALLOWED_ROLES = ['admin', 'mad professor', 'potato'];
    private function __construct(string $role)
    {
        Assert::oneOf($role, self::ALLOWED_ROLES);
        // ... rest of the implementation ...
    }
}

Value types are not identifiers to be looked up: they are just values. Don't design a table called colours if you know that all possible Colour implementations are already covered by your value type (that you can also convert to DDL constraints, by the way).

For anything that doesn't have upfront known identifiers, you have 3 choices:

  1. explicit validation via EntityManager#find() or equivalent
  2. trust input and use EntityManager#getReference() - I suggest against it
  3. explicit validation via EntityManager#find() and a L2 cache configuration (which avoids the DB hit)
@Ocramius commented on GitHub (Jul 23, 2018): > ```php > $type = ContactTypeRepo::find(1); > ``` Please don't do that in first place: it is overall a bad idea to rely on static global state for this sort of operation > the annotation (what i am asking) would do: > ```php > @Column(type="integer", lookup_type="App\Models\ContactType") > ``` This is still incorrect: it's either an `int` or a `App\Models\ContactType`. If you want you can use a `int|App\Models\ContactType` inside your DBAL type (what you proposed in the original issue description), but this is clearly a bad practice. > TL;DR: instead of fetching for an entity from entitymanager/repo, just allow setting the Identifier for the entity Use `EntityManager#getReference()` if you are already confident that the value exists, and you rely on foreign key integrity for the check: it converts a given identifier into a proxy or an existing entity of the requested type. > > In your case, I think that a lookup table is the incorrect approach, and that a value object in userland is a better alternative. > > Uhm, what? Another scenario for this might `User - Role` relationship. How can you ensure the integrity if you would only do `ENUMS` in application layer? That's how I understand what you said @Ocramius One thing is value types, the other is having maps with arbitrary identifiers. For value types, application-level constraints via value objects are more than sufficient: ```php final class Role { private const ALLOWED_ROLES = ['admin', 'mad professor', 'potato']; private function __construct(string $role) { Assert::oneOf($role, self::ALLOWED_ROLES); // ... rest of the implementation ... } } ``` Value types are not identifiers to be looked up: they are just values. Don't design a table called `colours` if you know that all possible `Colour` implementations are already covered by your value type (that you can also convert to DDL constraints, by the way). For anything that doesn't have upfront known identifiers, you have 3 choices: 1. explicit validation via `EntityManager#find()` or equivalent 2. trust input and use `EntityManager#getReference()` - I suggest against it 3. explicit validation via `EntityManager#find()` and a L2 cache configuration (which avoids the DB hit)
Author
Owner

@oadam commented on GitHub (Nov 2, 2021):

I agree that value types is the right approach.
However, I see only benefits to duplicates your value types in the database + foreign key.
This seems to be the consensus as well on stackoverflow : https://softwareengineering.stackexchange.com/questions/298472/is-it-wasteful-to-create-a-new-database-table-instead-of-using-enum-data-type

@oadam commented on GitHub (Nov 2, 2021): I agree that value types is the right approach. However, I see only benefits to duplicates your value types in the database + foreign key. This seems to be the consensus as well on stackoverflow : https://softwareengineering.stackexchange.com/questions/298472/is-it-wasteful-to-create-a-new-database-table-instead-of-using-enum-data-type
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6024