Final methods in Admin classes
wiistriker opened this issue · 1 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 inSonata\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, butgetNormalizedIdentifier
andgetUrlSafeIdentifier
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.