doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.86k stars 2.5k forks source link

Error in ProxyFactory using cloned proxy entity with Enum field #11386

Closed valkars closed 3 months ago

valkars commented 3 months ago

Bug Report

Q A
BC Break no
Version 3.1.0

Summary

I'll use simulated example. We have 2 entities (Cart, Customer) with OneToOne join. Customer entity has PHP Enum field. I fetch Cart from database, get Customer (receive proxy object), make copy with clone. When I try to access data of cloned entity - there is fatal error: Typed property ReflectionProperty::$name must not be accessed before initialization in Doctrine\ORM\Proxy\ProxyFactory line 237.

foreach ($class->getReflectionProperties() as $property) {
    if (! $property || ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
        continue;
    }

In that cycle $property is a Doctrine\Persistence\Reflection\EnumReflectionProperty object and $name variable is not initialized. EnumReflectionProperty has method getName() - using this method in ProxyFactory solves the problem.

public function getName(): string
{
    return $this->originalReflectionProperty->getName();
}

Possible fix:

foreach ($class->getReflectionProperties() as $property) {
    if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
        continue;

Current behavior

Fatal error

How to reproduce

2 entities, enum object and example to reproduce:

<?php

namespace App\Entity;

use App\Repository\CartRepository;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CartRepository::class)]
class Cart
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column]
    private ?int $amount = null;

    #[ORM\OneToOne(inversedBy: 'cart', cascade: ['persist', 'remove'])]
    private ?Customer $customer = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getAmount(): ?int
    {
        return $this->amount;
    }

    public function setAmount(int $amount): static
    {
        $this->amount = $amount;

        return $this;
    }

    public function getCustomer(): ?Customer
    {
        return $this->customer;
    }

    public function setCustomer(?Customer $customer): self
    {
        $this->customer = $customer;

        return $this;
    }
}
<?php

namespace App\Entity;

use App\Enum\Type;
use App\Repository\CustomerRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: CustomerRepository::class)]
class Customer
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    private ?string $name = null;

    #[ORM\Column(type: Types::SMALLINT, nullable: true, enumType: Type::class, options: ['unsigned' => true])]
    private ?Type $type = null;

    #[ORM\OneToOne(mappedBy: 'customer')]
    private ?Cart $cart = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): static
    {
        $this->name = $name;

        return $this;
    }

    public function getType(): ?Type
    {
        return $this->type;
    }

    public function setType(Type $type): static
    {
        $this->type = $type;

        return $this;
    }

    public function getCart(): ?Cart
    {
        return $this->cart;
    }

    public function setCart(?Cart $cart): self
    {
        // unset the owning side of the relation if necessary
        if (null === $cart && null !== $this->cart) {
            $this->cart->setCustomer(null);
        }

        // set the owning side of the relation if necessary
        if (null !== $cart && $cart->getCustomer() !== $this) {
            $cart->setCustomer($this);
        }

        $this->cart = $cart;

        return $this;
    }
}
<?php

namespace App\Enum;

enum Type: int
{
    case MALE = 1;
    case FEMALE = 2;
}
$cart = $repo->find(1);
$customer = clone $cart->getCustomer();
echo $customer->getName();

Expected behavior

No errors

greg0ire commented 3 months ago

Are you using https://github.com/doctrine/persistence/releases/tag/3.3.2 ?

valkars commented 3 months ago
"name": "doctrine/persistence",
"version": "3.3.1",
greg0ire commented 3 months ago

Please update.

valkars commented 3 months ago

Update to 3.3.2 does not help, error remains

valkars commented 3 months ago

Because bug in ProxyFactory, not in Persistence package

greg0ire commented 3 months ago

Oh right, I read too fast, and I think your fix is correct :thinking: , in fact I'm a bit surprised I didn't contribute it after contributing https://github.com/doctrine/persistence/pull/348 (which was already in 3.3.1)

greg0ire commented 3 months ago

I see you're using 3.1.0, maybe this is just a matter of merging up and releasing a new version, let me check.

greg0ire commented 3 months ago

Nope, it's not contributed to 2.x yet. Please send a PR :pray:

greg0ire commented 3 months ago

I think this only affects 3.1.x, because this is the branch where I contributed https://github.com/doctrine/orm/pull/11330

valkars commented 3 months ago

Made a PR