phpdoctrine-orm

How to refactor doctrine property names while retaining/aliasing the old names?


Hi I'm working with a large legacy library that is used across a dozen projects, some of the entities are not following PS12 naming but despite the fact they are private properties it is not so easy to rename them because of DQL and the doctrine repository findBy methods that currently using the old names.

To avoid a breaking change to the library is there a way of renaming the properties while continuing to allow usage of the old names?

If doctrine didn't use reflection, something like the following would be ideal:

class DummyEntity
{
    use DeprecatedEntityPropertyNames;

    // formerly private string $new_name;
    private string $newName;

    public function getNewName(): string
    {
        return $this->newName;
    }

    public function setNewName(string $newName): static
    {
        $this->newName = $newName;
        return $this;
    }
}

trait DeprecatedEntityPropertyNames
{
    private function snakeToCamel(string $input): string
    {
        return lcfirst(str_replace('_', '', ucwords($input, '_')));
    }

    public function __get(string $name): mixed
    {
        $candidateName = $this->snakeToCamel($name, '_');
        if (isset($this->$candidateName)) {
            trigger_deprecation('my-library', 'x.x', "property $name accessed via old snake_case name");
            return $this->$candidateName;
        }
        $traitName = static::class;
        $className = $this::class;
        throw new Error("undefined property $name in $className, this class has the trait $traitName, have tried and failed to use $candidateName");
    }

    public function __set(string $name, mixed $value): void
    {
        $candidateName = $this->snakeToCamel($name, '_');
        // bad method name isDefault means is this property declared (instead of being created dynamically)
        if ((new ReflectionProperty($this, $candidateName))->isDefault()) {
            trigger_deprecation('my-library', 'x.x', "property $name accessed via old snake_case name");
            $this->$candidateName = $value;
            return;
        }
        $traitName = static::class;
        $className = $this::class;
        throw new Error("undefined property $name in $className, this class has the trait $traitName, have tried and failed to use $candidateName");
    }

    public function __isset(string $name): bool
    {
        $candidateName = $this->snakeToCamel($name, '_');
        // bad method name isDefault means is this property declared (instead of being created dynamically)
        if ((new ReflectionProperty($this, $candidateName))->isDefault()) {
            if ($name !== $candidateName) {
                trigger_deprecation('my-library', 'x.x', "property $name accessed via old snake_case name");
            }
            return isset($this->$candidateName);
        } else {
            return false;
        }
    }

    public function __unset(string $name): void
    {
        $candidateName = $this->snakeToCamel($name, '_');
        if ($name !== $candidateName) {
            trigger_deprecation('my-library', 'x.x', "property $name accessed via old snake_case name");
        }
        unset($this->$candidateName);
    }
}

there will be deprecation notices that allows clean-up, an application at a time before eventually removing the old naming without having to make a breaking change to the library

however since doctrine uses reflection to get and set properties the reflection bypasses the magic getter/setter, is there an alternative way to work with both newName and new_name?


Solution

  • 1 Use Doctrine’s #[ORM\Column(name: "new_name")]:

    #[ORM\Column(name: "new_name", type: "string")] private string $newName;

    2. Want Deprecation Warning? → Use Custom Repository:

    class DummyEntityRepository extends \Doctrine\ORM\EntityRepository
    {
        public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
        {
            if (isset($criteria['new_name'])) {
                trigger_deprecation('my-lib', '1.0', '"new_name" is deprecated. Use "newName".');
                $criteria['newName'] = $criteria['new_name'];
                unset($criteria['new_name']);
            }
    
            return parent::findBy($criteria, $orderBy, $limit, $offset);
        }
    }
    

    3. Register the custom repository in the entity:

    #[ORM\Entity(repositoryClass: DummyEntityRepository::class)]

    No breaking changes. Old keys work. Deprecation warning works. Future-safe.