sonata-project / SonataAdminBundle

The missing Symfony Admin Generator

Home Page:https://docs.sonata-project.org/projects/SonataAdminBundle

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Final methods in Admin classes

wiistriker opened this issue · comments

Hello! First of all thanks for great library which helps me a lot many years.

I use sonata admin bundle with doctrine orm admin bundle

Just moved from 3.x to 4.x and faced with some circumstances which lead me to boilerplate code.

For one entity i have data partitioning mechanism which can store same entity in several identical tables in database. Lets call this entity Product. In database for example we have tables:

products_001
products_002
products_00N

To know where each product stored i use complicated ID in url, like:

001S123 --- product with id = 123 stored in table products_001
002S321 --- product with id = 321 stored in table products_002

and so on.

Later it worked:

class ProductAdmin extends AbstractAdmin
{
    public function id(object $model): ?string
    {
        return $model->getFullId();
    }

    public function getUrlsafeIdentifier(object $model): ?string
    {
        return $model->getFullId();
    }

    public function getNormalizedIdentifier(object $model): ?string
    {
        return $model->getFullId();
    }

    public function getObject($id): ?object
    {
        $em = $this->modelManager->getEntityManager($this->getClass());

        // method findByFullId know in what partition it should seek for entity with id
        $object = $em->getRepository($this->getClass())->findByFullId($id);
        foreach ($this->getExtensions() as $extension) {
            $extension->alterObject($this, $object);
        }

        return $object;
    }

This worked but getUrlsafeIdentifier, getNormalizedIdentifier, getObject will be marked as @Final and after upgrade from 3.x to 4.x i cant use this method.

Moving forward. This methods call ModelManagerInterface methods with the same name, so i try to change this behaviour in
Sonata\DoctrineORMAdminBundle\Model\ModelManager but this class is also final, so i ended up with such ugly code:

class ProductsModelManager implements ModelManagerInterface, LockInterface, ProxyResolverInterface
{
    protected ModelManager $doctrineModelManager;

    public function __construct(ModelManager $doctrineModelManager)
    {
        $this->doctrineModelManager = $doctrineModelManager;
    }

    public function getLockVersion(object $object)
    {
        return $this->doctrineModelManager->getLockVersion($object);
    }

    public function lock(object $object, ?int $expectedVersion): void
    {
        $this->doctrineModelManager->lock($object, $expectedVersion);
    }

    public function create(object $object): void
    {
        $this->doctrineModelManager->create($object);
    }

    public function update(object $object): void
    {
        $this->doctrineModelManager->update($object);
    }

    public function delete(object $object): void
    {
        $this->doctrineModelManager->delete($object);
    }

    public function findBy(string $class, array $criteria = []): array
    {
        return $this->doctrineModelManager->findBy($class, $criteria);
    }

    public function findOneBy(string $class, array $criteria = []): ?object
    {
        return $this->doctrineModelManager->findOneBy($class, $criteria);
    }

    public function find(string $class, $id): ?object
    {
        if ($class === Product::class) {
            $productsRepository = $this->doctrineModelManager->getEntityManager($class)->getRepository(Product::class);
            return $productsRepository->findById($id);
        }

        return $this->doctrineModelManager->find($class, $id);
    }

    public function batchDelete(string $class, ProxyQueryInterface $query): void
    {
        $this->doctrineModelManager->batchDelete($class, $query);
    }

    public function createQuery(string $class): ProxyQueryInterface
    {
        return $this->doctrineModelManager->createQuery($class);
    }

    public function getIdentifierValues(object $model): array
    {
        return $this->doctrineModelManager->getIdentifierValues($model);
    }

    public function getIdentifierFieldNames(string $class): array
    {
        return $this->doctrineModelManager->getIdentifierFieldNames($class);
    }

    public function getNormalizedIdentifier(object $model): ?string
    {
        if ($model instanceof Product) {
            return $model->getId();
        }

        return $this->doctrineModelManager->getNormalizedIdentifier($model);
    }

    public function getUrlSafeIdentifier(object $model): ?string
    {
        if ($model instanceof Product) {
            return $model->getId();
        }

        return $this->doctrineModelManager->getUrlSafeIdentifier($model);
    }

    public function reverseTransform(object $object, array $array = []): void
    {
        $this->doctrineModelManager->reverseTransform($object, $array);
    }

    public function supportsQuery(object $query): bool
    {
        return $this->doctrineModelManager->supportsQuery($query);
    }

    public function executeQuery(object $query)
    {
        return $this->doctrineModelManager->executeQuery($query);
    }

    public function getExportFields(string $class): array
    {
        return $this->doctrineModelManager->getExportFields($class);
    }

    public function addIdentifiersToQuery(string $class, ProxyQueryInterface $query, array $idx): void
    {
        $this->doctrineModelManager->addIdentifiersToQuery($class, $query, $idx);
    }

    public function getRealClass(object $object): string
    {
        return $this->doctrineModelManager->getRealClass($object);
    }
}

I write 20 methods just to change behaviour in 3 methods.

What do you think? Is it really necessary to have final classes and methods in such wide-used library? And what is a logick in deciding which methods will be final and what will be extendable? For example AbstractAdmin::id method is not final, but getNormalizedIdentifier and getUrlSafeIdentifier is

Moving forward. This methods call ModelManagerInterface methods with the same name, so i try to change this behaviour in Sonata\DoctrineORMAdminBundle\Model\ModelManager but this class is also final, so i ended up with such ugly code:

class ProductsModelManager implements ModelManagerInterface, LockInterface, ProxyResolverInterface
{
    protected ModelManager $doctrineModelManager;

    public function __construct(ModelManager $doctrineModelManager)
    {
        $this->doctrineModelManager = $doctrineModelManager;
    }

    public function getLockVersion(object $object)
    {
        return $this->doctrineModelManager->getLockVersion($object);
    }

    public function lock(object $object, ?int $expectedVersion): void
    {
        $this->doctrineModelManager->lock($object, $expectedVersion);
    }

    public function create(object $object): void
    {
        $this->doctrineModelManager->create($object);
    }

    public function update(object $object): void
    {
        $this->doctrineModelManager->update($object);
    }

    public function delete(object $object): void
    {
        $this->doctrineModelManager->delete($object);
    }

    public function findBy(string $class, array $criteria = []): array
    {
        return $this->doctrineModelManager->findBy($class, $criteria);
    }

    public function findOneBy(string $class, array $criteria = []): ?object
    {
        return $this->doctrineModelManager->findOneBy($class, $criteria);
    }

    public function find(string $class, $id): ?object
    {
        if ($class === Product::class) {
            $productsRepository = $this->doctrineModelManager->getEntityManager($class)->getRepository(Product::class);
            return $productsRepository->findById($id);
        }

        return $this->doctrineModelManager->find($class, $id);
    }

    public function batchDelete(string $class, ProxyQueryInterface $query): void
    {
        $this->doctrineModelManager->batchDelete($class, $query);
    }

    public function createQuery(string $class): ProxyQueryInterface
    {
        return $this->doctrineModelManager->createQuery($class);
    }

    public function getIdentifierValues(object $model): array
    {
        return $this->doctrineModelManager->getIdentifierValues($model);
    }

    public function getIdentifierFieldNames(string $class): array
    {
        return $this->doctrineModelManager->getIdentifierFieldNames($class);
    }

    public function getNormalizedIdentifier(object $model): ?string
    {
        if ($model instanceof Product) {
            return $model->getId();
        }

        return $this->doctrineModelManager->getNormalizedIdentifier($model);
    }

    public function getUrlSafeIdentifier(object $model): ?string
    {
        if ($model instanceof Product) {
            return $model->getId();
        }

        return $this->doctrineModelManager->getUrlSafeIdentifier($model);
    }

    public function reverseTransform(object $object, array $array = []): void
    {
        $this->doctrineModelManager->reverseTransform($object, $array);
    }

    public function supportsQuery(object $query): bool
    {
        return $this->doctrineModelManager->supportsQuery($query);
    }

    public function executeQuery(object $query)
    {
        return $this->doctrineModelManager->executeQuery($query);
    }

    public function getExportFields(string $class): array
    {
        return $this->doctrineModelManager->getExportFields($class);
    }

    public function addIdentifiersToQuery(string $class, ProxyQueryInterface $query, array $idx): void
    {
        $this->doctrineModelManager->addIdentifiersToQuery($class, $query, $idx);
    }

    public function getRealClass(object $object): string
    {
        return $this->doctrineModelManager->getRealClass($object);
    }
}

I write 20 methods just to change behaviour in 3 methods.

Seems like you found the solution then.

The issue about the 20 methods to override is because the ModelManager is a too-big interface and should be split. But that would require a lot of work.

What do you think? Is it really necessary to have final classes and methods in such wide-used library?

Yes. This is especially because this library is wide-used that class/methods must be final. To allow them to change more easily without introducing BC-break. We encountered 7 years without a major version, with really hard work to improve things because nothing was final and we had to keep tons of deprecated code.

I can find you tons of reference:
https://matthiasnoback.nl/2018/09/final-classes-by-default-why/
symfony/symfony#15233
https://ocramius.github.io/blog/when-to-declare-classes-final/

And what is a logick in deciding which methods will be final and what will be extendable? For example AbstractAdmin::id method is not final, but getNormalizedIdentifier and getUrlSafeIdentifier is

getNormalizedIdentifier or getUrlSafeIdentifier have an extension point, the modelManager.
id has none (you might want a different result than getNormalizedIdentifier), so we cannot make it final so far even if we want to ; it could require a method inside the model manager.