Migrate Gedmo to KnpLabs

This post was updated at November 2022 with fresh know-how.
What is new?

Switched from deprecated --set option to rector.php config. Removed Rector upgrade set, as outdated and could cause troubles. Better handled individually per project.


With Symfony 5 upgrade, we need any working Doctrine behaviors. <br> Month later, we have KnpLabs\DoctrineBehaviors 2.0 with full Symfony 5 support.

If you used older Doctrine Behaviors, you're covered with Rector migration path. <br> But what if you're using old broken Gedmo?

I'll show you how you can migrate Gedmo to KnpLabs.

Pick behavior your want to migrate from Gedmo to KnpLabs:


If you use other Gedmo behavior that is not listed here, you might request or better add it to KnpLabs repository via pull-request. I assure you I'll provide stable maintenance support for KnpLabs package. It's not that hard with CI on steroids it has now.


1. Migrate Timestampable

Gedmo

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity;

/**
 * @ORM\Entity
 */
class Meetup
{
    use TimestampableEntity;
}

KnpLabs

  • Replace trait with Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait
  • Add Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface interface
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;
use Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface;

/**
 * @ORM\Entity
 */
class Meetup implements TimestampableInterface
{
    use TimestampableTrait;
}

2. Migrate Sluggable

Gedmo

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity
 */
class Meetup
{
    /**
     * @Gedmo\Slug(fields={"name"})
     */
    private $slug;

    public function getSlug(): ?string
    {
        return $this->slug;
    }

    public function setSlug(?string $slug): void
    {
        $this->slug = $slug;
    }
}

KnpLabs

  • Add Knp\DoctrineBehaviors\Model\Sluggable\SluggableTrait trait

  • Add Knp\DoctrineBehaviors\Contract\Entity\SluggableInterface interface

  • Remove getSlug()/setSlug() method that is already in trait

  • Replace * @Gedmo\Slug(fields={"name"}) with getSluggableFields() method that contains fields

    E.g., from:

use Gedmo\Mapping\Annotation as Gedmo;

// ...

/**
 * @Gedmo\Slug(fields={"name", "surname"})
 */
private $slug;

to


/**
 * @return string[]
 */
public function getSluggableFields(): array
{
    return ['name', 'surname'];
}

In full code:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model\Sluggable\SluggableTrait;
use Knp\DoctrineBehaviors\Contract\Entity\SluggableInterface;

/**
 * @ORM\Entity
 */
class Category implements SluggableInterface
{
    use SluggableTrait;

    /**
     * @return string[]
     */
    public function getSluggableFields(): array
    {
        return ['name'];
    }
}

3. Migrate Tree

This one will be tricky, because:

  • Gedmo supports nested-set, closure-table and materialized-path.
  • KnpLabs currently supports materialized path

So if you're using anything but materialized path in Gedmo, you'll have to migrate PHP code (see below) + migrate your database data to materialized path (write your migration or Google one).


The PHP migration looks like this:

Gedmo

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @ORM\Entity
 * @Gedmo\Tree(type="nested")
 */
class Category
{
    /**
     * @Gedmo\TreeLeft
     * @ORM\Column(name="lft", type="integer")
     * @var int
     */
    private $lft;

    /**
     * @Gedmo\TreeRight
     * @ORM\Column(name="rgt", type="integer")
     * @var int
     */
    private $rgt;

    /**
     * @Gedmo\TreeLevel
     * @ORM\Column(name="lvl", type="integer")
     * @var int
     */
    private $lvl;

    /**
     * @Gedmo\TreeRoot
     * @ORM\ManyToOne(targetEntity="Category")
     * @ORM\JoinColumn(name="tree_root", referencedColumnName="id", onDelete="CASCADE")
     * @var Category
     */
    private $root;

    /**
     * @Gedmo\TreeParent
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
     * @var Category
     */
    private $parent;

    /**
     * @ORM\OneToMany(targetEntity="Category", mappedBy="parent")
     * @var Category[]|Collection
     */
    private $children;

    public function getRoot(): self
    {
        return $this->root;
    }

    public function setParent(self $category): void
    {
        $this->parent = $category;
    }

    public function getParent(): self
    {
        return $this->parent;
    }
}

KnpLabs

  • Add Knp\DoctrineBehaviors\Model\Tree\TreeNodeTrait trait
  • Add Knp\DoctrineBehaviors\Contract\Entity\TreeNodeInterface interface
  • Remove all tree related properties and methods, since they're in trait now
  • Remove Gedmo annotations
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Knp\DoctrineBehaviors\Contract\Entity\TreeNodeInterface;
use Knp\DoctrineBehaviors\Model\Tree\TreeNodeTrait;

/**
 * @ORM\Entity
 */
class Category implements TreeNodeInterface
{
    use TreeNodeTrait;
}

4. Migrate Translatable

What about Performance?

I recall picking the behavior package for new Lekarna.cz 6 years ago. I was young, and a quantity was more than quality to me, so I was leaning towards Gedmo since it had more downloads.

But it's performance surprised me. Why? The translated item had 1:many dependency on translation table, so for every single item, it joined an X extra lines forever single translated column.

So even if you use Symfony 4 and everything works well for you, consider comparing performance with KnpLabs translations. Who knows, it might get your multi-lingual application 10x faster.


Gedmo

namespace App\Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Translatable\Translatable;

/**
 * @ORM\Entity
 */
class Category implements Translatable
{
    /**
     * @Gedmo\Translatable
     * @ORM\Column(length=128)
     */
    private $title;

    /**
     * @Gedmo\Locale
     */
    private $locale;

    public function setTitle($title)
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function setTranslatableLocale($locale)
    {
        $this->locale = $locale;
    }
}

KnpLabs

  • Replace interface with Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface
  • Add Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait
  • Remove all translated fields and locale methods from main entity
  • Remove Gedmo annotations
namespace App\Entity;

use Knp\DoctrineBehaviors\Model\Translatable\TranslatableTrait;
use Knp\DoctrineBehaviors\Contract\Entity\TranslatableInterface;

class Category implements TranslatableInterface
{
    use TranslatableTrait;
}

So, where is the title property we need to translate? Every translated property is in the new <entity>Translation class. This approach makes sure that the complexity of 1 item with dozens of translation stays 1:1 = it's super fast!

namespace App\Entity;

use Knp\DoctrineBehaviors\Contract\Entity\TranslationInterface;
use Knp\DoctrineBehaviors\Model\Translatable\TranslationTrait;

class CategoryTranslation implements TranslationInterface
{
    use TranslationTrait;

    /**
     * @ORM\Column(length=128)
     */
    private $title;
}

In short:

  • Translatable - the primary entity you use in your code
  • Translation - the helper entity with translated items

Usage stays the same:

$category->getTitle();

That's all for the migration. Oh, you're still reading? Are you waiting for some easy solution to cover it all?

5. Migrate Blameable

Gedmo

namespace App\Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Category
{
    /**
     * @Gedmo\Blameable(on="create")
     */
    private $createdBy;

    /**
     * @Gedmo\Blameable(on="update")
     */
    private $updatedBy;

    public function getCreatedBy()
    {
        return $this->createdBy;
    }

    public function getUpdatedBy()
    {
        return $this->updatedBy;
    }
}

KnpLabs

  • Add Knp\DoctrineBehaviors\Contract\Entity\BlameableInterface interface
  • Add Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait trait
  • Remove Gedmo annotations
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Contract\Entity\BlameableInterface;
use Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait;

/**
 * @ORM\Entity
 */
class Category implements BlameableInterface
{
    use BlameableTrait;
}

6. Migrate Loggable

Gedmo

namespace App\Entity;

use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @Gedmo\Loggable
 */
class Category
{
    /**
     * @Gedmo\Versioned
     * @ORM\Column(name="title", type="string", length=8)
     */
    private $title;
}

KnpLabs

  • Add Knp\DoctrineBehaviors\Model\Loggable\LoggableTrait trait
  • Add Knp\DoctrineBehaviors\Contract\Entity\LoggableInterface interface
  • Remove Gedmo annotations
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Model\Loggable\LoggableTrait;
use Knp\DoctrineBehaviors\Contract\Entity\LoggableInterface;

/**
 * @ORM\Entity
 */
class Category implements LoggableInterface
{
    use LoggableTrait;

    /**
     * @ORM\Column(name="title", type="string", length=8)
     */
    private $title;
}

7. Migrate SoftDeletable

Gedmo

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;

/**
 * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false, hardDelete=true)
 * @ORM\Entity
 */
class Category
{
    /**
     * @ORM\Column(name="deletedAt", type="datetime", nullable=true)
     */
    private $deletedAt;

    public function getDeletedAt()
    {
        return $this->deletedAt;
    }

    public function setDeletedAt($deletedAt)
    {
        $this->deletedAt = $deletedAt;
    }
}

KnpLabs

  • Add Knp\DoctrineBehaviors\Contract\Entity\SoftDeletableInterface interface
  • Add Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait trait
  • Remove Gedmo annotations
namespace App\Entity;

use Knp\DoctrineBehaviors\Contract\Entity\SoftDeletableInterface;
use Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait;

class Category implements SoftDeletableInterface
{
    use SoftDeletableTrait;
}

Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!