sonata-project / SonataAdminBundle

The missing Symfony Admin Generator
https://docs.sonata-project.org/projects/SonataAdminBundle
MIT License
2.11k stars 1.26k forks source link

Admin ServiceSubscriber + autoconfigure #7657

Closed tdumalin closed 2 years ago

tdumalin commented 2 years ago

Feature Request

Hi,

I think it would be a good idea that Admin implements ServiceSubscriberInterface and use symfony autoConfigure https://symfony.com/doc/current/service_container/service_subscribers_locators.html

With those two features admin can be created without any configuration file, and any service can be injected easily.

Here is my personal implementation to make it possible:

  1. Interface to identify all admin that should be autoconfigured
    
    //src/Admin/AutoConfiguredAdminInterface
    <?php

namespace App\Admin;

interface AutoConfiguredAdminInterface { public static function getDefaultConfig(): array; }


2. Create an AbstractAdmin with shared behavior

```php
<?php

//src/Admin/AbstractAdmin
namespace App\Admin;

use App\Entity\User;
use Psr\Container\ContainerInterface;
use Sonata\AdminBundle\Admin\AbstractAdmin as BaseAbstractAdmin;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Service\Attribute\SubscribedService;
use Symfony\Contracts\Service\ServiceSubscriberInterface;
use Symfony\Contracts\Service\ServiceSubscriberTrait;

abstract class AbstractAdmin extends BaseAbstractAdmin implements ServiceSubscriberInterface, AutoConfiguredAdminInterface
{
    use ServiceSubscriberTrait;

    /** @var ContainerInterface */
    protected $container;

    public function __construct($code, $class, $baseControllerName, ContainerInterface $container)
    {
        parent::__construct($code, $class, $baseControllerName);
        $this->container = $container;
    }

    public static function getDefaultConfig(): array
    {
        //contains construct arguments + tag attributes
        return [
            'class' => static::getDefaultClass(),
            'controller' => null,
            'label' => static::getDefaultLabel(),
            'show_in_dashboard' => true,
            'group' => 'admin',
            'label_catalogue' => null,
            'icon' => null,
            'on_top' => false,
            'keep_open' => false,
            'manager_type' => 'orm'
        ];
    }

    public static function getDefaultClass()
    {
        //return App\Entity\Product for App\Admin\ProductAdmin
        $explode = explode("\\", static::class);
        return "App\\Entity\\" . str_replace("Admin", "", end($explode));
    }

    public static function getDefaultLabel(): string
    {
         //return Product for App\Admin\ProductAdmin
        $explode = explode("\\", static::class);
        return str_replace("Admin", "", end($explode));
    }

    //some services you want to retrieve in all your admins
    #[SubscribedService()]
    protected function tokenStorage(): TokenStorageInterface
    {
        return $this->container->get(__METHOD__);
    }

    protected function getUser(): User
    {
        return $this->tokenStorage()->getToken()->getUser();
    }
}
  1. Create Admin(s) that extends the custom AbstractAdmin
<?php

//src/Admin/AbstractAdmin
namespace App\Admin;

use App\Product\AwesomeService;
use App\Controller\ProductAdminController
use Symfony\Contracts\Service\Attribute\SubscribedService;

class ProductAdlin extends AbstractAdmin //current folder abstractAdmin
{
    //don't forget this to inject custom service
    use ServiceSubscriberTrait;

    public static function getDefaultConfig(): array
    {
        return array_merge(parent::getDefaultConfig(),[
            //override any default value here
            'controller' => ProductAdminController::class, 
        ]);
    }

    #[SubscribedService()]
    protected function awesomeService(): AwesomeService
    {
        return $this->container->get(__METHOD__);
    }

    protected function configure() : void
    {
         //add here any configuration setted with services "calls" configuration
         $this->setTemplate('edit', 'admin/product/edit.html.twig');
         $this->addChild($this->getConfigurationPool()->getAdminByAdminCode(ProductVariantAdmin::class),'product');

    }
}
  1. Create compiler
<?php

//src/DependencyInjection/Compiler
namespace App\DependencyInjection\Compiler;

use Sonata\AdminBundle\DependencyInjection\Admin\TaggedAdminInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppAdminPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $taggedServices = $container->findTaggedServiceIds("app.admin");
        foreach ($taggedServices as $serviceId => $tag) {
            $definition = $container->getDefinition($serviceId);
            $class = $definition->getClass();
            $config = call_user_func([$class, 'getDefaultConfig']);

            $definition->setArgument(0, null);
            $definition->setArgument(1, $config['class']);
            $definition->setArgument(2, $config['controller']);

            $definition->addTag(TaggedAdminInterface::ADMIN_TAG, $config);
        }
    }
}
  1. Use compiler
<?php

//src/Kernel.php
namespace App;

use App\Admin\AutoConfiguredAdminInterface;
use App\DependencyInjection\Compiler\AppAdminPass;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel
{
    use MicroKernelTrait;
    //...
    protected function build(ContainerBuilder $container)
    {
        //auto configure all admin that implements interface
        $container->registerForAutoconfiguration(AutoConfiguredAdminInterface::class)
            ->addTag('app.admin');
        //set priority hier that default the one used by Sonata\AdminBundle\DependencyInjection\Compiler\AddDependencyCallsCompilerPass
        $container->addCompilerPass(new AppAdminPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1);
    }
}

That's it, now any service that extends AbstractAdmin doesn't need any configuration !

VincentLanglet commented 2 years ago

Seems like you already had this idea https://github.com/sonata-project/SonataAdminBundle/issues/6551 Maybe it's better to open the previous issue instead, WDYT ?

This could be great to have a way to avoid using config file.

Feel free to provide a PR.

tdumalin commented 2 years ago

Yes that's true i already have opened an issue for this, but this time I manage to make it work! You are right i will reopen the old one and make a PR when i have some free time that seems a better idea.

Thanks