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 supports ServiceSubscriberInterface and configure admin directely in file #6551

Closed tdumalin closed 2 years ago

tdumalin commented 4 years ago

Feature Request

Use the ServiceSubscriberInterface and some static function to autoconfigure admins, so no need to us a xml/yaml/php file to configure admin, Here is an example of the idea:

<?php

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin as BaseAbstract;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

abstract class AbstractAdmin extends BaseAbstract implements ServiceSubscriberInterface
{
    private $locator;

    public function __construct(ContainerInterface $locator)
    {
        parent::__construct($this->getAutoGeneratedCode(), $this->getClass(), $this->getBaseControllerName());
        $this->locator = $locator;
    }

    public static function getSubscribedServices()
    {
        return [
            //some services used by all admins like request for instance
            RequestStack::class,
        ];
    }

    public function getAutoGeneratedCode()
    {
        return strtolower(str_replace(["\\", 'Admin'], ['.', ''], $this->fromCamelCase(self::class)));
    }

    public static function fromCamelCase($input)
    {
        preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches);
        $ret = $matches[0];
        foreach ($ret as &$match) {
            $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match);
        }
        return implode('_', $ret);
    }

    public function get(string $id)
    {
        return $this->locator->get($id);
    }

    public function getRequest()
    {
        return $this->get(RequestStack::class)->getCurrentRequest();
    }
}

The configure all required stuff for an admin:

<?php

namespace App\Admin;

use App\Entity\Product;
use App\Slugger\Slugger;
use App\Controller\Admin\ProductAdminController;

/*
    This file standfor this service declaraion
    app.admin.product:
        class: App\Admin\Product
        arguments: [~ , App\Entity\Product,App\Controller\Admin\ProductAdminController]
        calls:
            - setSlugger: ['@app.slugger']
            - setTemplate: ['edit','admin/product_image/edit.html.twig']
        tags:
            - { name: sonata.admin, manager_type: orm }            
        public: true  
*/
class ProductAdmin extends AbstractAdmin
{
    public static function getSubscribedServices()
    {
        return array_merge(parent::getSubscribedServices(),[
            //some services
            Slugger::class,
        ]);
    }

    public function configure()
    {
        $this->getTemplateRegistry()->setTemplate('edit','admin/product_image/edit.html.twig');

        parent::configure();        
    }

    public static function getClass()
    {
        return Product::class;
    }    

    public static function getControllerBaseName()
    {
        return ProductAdminController::class;
    }

    public static function getManagerType()
    {
        return 'orm';
    }
}
mleko64 commented 3 years ago

Hello. I did something like this in my project. I have a "BaseAdmin" class:

namespace App\Admin;

use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Service\ServiceSubscriberInterface;

/**
 * Handles base admin methods.
 *
 * Must be extended by every admin class.
 */
abstract class BaseAdmin extends AbstractAdmin implements ServiceSubscriberInterface
{
    /**
     * Global services for all Admin classes.
     */
    private const GLOBAL_SERVICES = [
        ParameterBagInterface::class,
        Security::class,
    ];

    /**
     * A small DI container.
     */
    private ContainerInterface $container;

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

    final public static function getSubscribedServices()
    {
        return array_unique(array_merge(static::registerServices(), self::GLOBAL_SERVICES));
    }

    /**
     * You can override this method in Admin class and register own services.
     * Registered services you can fetch via "getContainer" method.
     */
    public static function registerServices(): array
    {
        return [];
    }

    /**
     * Returns small DI container.
     */
    public function getContainer(): ContainerInterface
    {
        return $this->container;
    }

    /**
     * Returns container parameter.
     *
     * @param mixed|null $default
     *
     * @return mixed|null
     */
    public function getContainerParameter(string $parameter, $default = null)
    {
        if ($this->getContainer()->get(ParameterBagInterface::class)->has($parameter)) {
            return $this->getContainer()->get(ParameterBagInterface::class)->get($parameter);
        }

        return $default;
    }
}

And in some Admin class:

namespace App\Admin\User;

use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class UserAdmin extends BaseAdmin
{
    public static function registerServices(): array
    {
        return [
            UserPasswordEncoderInterface::class,
        ];
    }

    /**
     * Updates user password.
     */
    private function updateUserPassword(User $user): void
    {
        if (!$user->getPlainPassword()) {
            return;
        }

        $passwordEncoder = $this->getContainer()->get(UserPasswordEncoderInterface::class);
        $user->setPassword($passwordEncoder->encodePassword($user, $user->getPlainPassword()));
        $user->eraseCredentials();
    }
}

I believe that in the future will be added full support for autowiring and you can inject needed service in the constructor.

tdumalin commented 3 years ago

Hi @mleko64, That's exactly what i need, I didn't know that adding the container as last construct arguments will be autowired. Thanks for the share !

sarim commented 3 years ago

It'd be nice to have this by default in v4.0. In recent symfony version autowiring is the recommended way. Both behavior can exist, and it could be a flag in admin generation command. Would the maintainers be interested in this? If so I can make a PR.

franmomu commented 3 years ago

Sure! I don't know if is going to make it to 4.0 or 5.0, but any help is welcomed. I thought about this some time ago and I wanted to revisit this, I'll throw some random ideas (haven't tried, so maybe ) I had just in case it helps:

One of the problems is to get rid of the __constructor of AbstractAdmin, right now there are 3 parameters: $code, $class and $baseControllerName.

I think we can get rid of $baseControllerName "easily", apparently it is only used in AbstractAdmin::buildRoutes, here:

https://github.com/sonata-project/SonataAdminBundle/blob/c68ea819adb41cd6d547c07eea7e75b28a7fd2b2/src/Admin/AbstractAdmin.php#L3289-L3296

So one way to remove this is to create a RouteBuilderInterface::create(AdminInterface $admin) method that would return the RouteCollection and that service would be in charge of retrieving the correct baseControllerName from a ControllerRegistry with a ControllerRegistry::get(AdminInterface $admin) that would return the proper baseControllerName for an Admin.

The $class argument is fine to me, we can keep it and for the $code one... we can create a method like initialize that the Compiler Pass would call it with the $code parameter (the id of the service).

After this, I thought about creating an abstract class, similar to the suggested here, something like:

<?php

//...

abstract class ServiceAbstractAdmin extends AbstractAdmin implements ServiceSubscriberInterface
{
    public function __construct(string $modelClass)
    {
        parent::__construct($modelClass);
    }

    /**
     * @internal
     */
    public function setContainer(ContainerInterface $container): void
    {
        $this->container = $container;
    }

    public static function getSubscribedServices(): array
    {
        return [
            // Here will go the persistence agnostic services like translator, validator, pool, security_handler, etc
        ];
    }
}

and then in the persistence bundles, create another abstract admin extending from this one and adding the persistence dependent services, so at the end, the user would extend from those admin classes.

As I said, this is some random thoughts I haven't tried, just thinking out loud.

VincentLanglet commented 3 years ago

Sure! I don't know if is going to make it to 4.0 or 5.0

The 4.0 milestone is almost finished and I think that we could try release the 4.0 version in January or February, so I would recommend the 5.0 milestone. The priority is to offer a proper Symfony 5 support ASAP, and this feature seems to introduce a lot of refactoring.

But any PR is welcomed.

sarim commented 3 years ago

There's two features being discussed here, one is make Admin support ServiceSubscriberInterface and 2nd is configure admin directly in file.

I'd like to implement configure admin directly in file. first.

@franmomu I was thinking of doing more in heavy lifting in Compiler Pass and leaving Admin as it is. We make new interface, ex: AutoconfigurableAdmin. If a service implements AutoconfigurableAdmin compiler pass will call its static methods and configure it.

image

What do you think?

VincentLanglet commented 3 years ago

Be aware that there is more configuration keys: https://github.com/sonata-project/SonataAdminBundle/blob/3.x/src/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php#L228-L241

Also, @wbloszyk is introducing another CompilerPass https://github.com/sonata-project/SonataAdminBundle/pull/6566. We should verify that your idea is compatible. (If we want to configure from the class, we should be able to configure everything)

sarim commented 3 years ago

Be aware that there is more configuration keys

If we want to configure from the class, we should be able to configure everything

yes, that makes sense. A static method for every attribute would be ugly AF. So these must be condensed into one function. But the constructor argument is staying same three vars. So I think

interface AutoconfigurableAdmin
{
    public static function getCode();

    public static function getEntityClass();

    public static function getControllerClass();

    public static function getAttributes();
}

getAttributes will return an array, others will return just a value.

github-actions[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

Hanmac commented 2 years ago

One thing for Thought:

VincentLanglet commented 2 years ago

Annotation are deprecated/archived.

For autoconfigure, an order option could be added.

eerison commented 2 years ago

Be aware that there is more configuration keys

If we want to configure from the class, we should be able to configure everything

yes, that makes sense. A static method for every attribute would be ugly AF. So these must be condensed into one function. But the constructor argument is staying same three vars. So I think

interface AutoconfigurableAdmin
{
    public static function getCode();

    public static function getEntityClass();

    public static function getControllerClass();

    public static function getAttributes();
}

getAttributes will return an array, others will return just a value.

Hey @sarim just to remember that constructor parameters are deprecated and it should be passed in services tags

admin.category:
        class: App\Admin\CategoryAdmin
        tags:
            - { name: sonata.admin, model_class: App\Entity\Category, controller: ~, manager_type: orm, label: Category }
core23 commented 2 years ago

Annotation are deprecated/archived.

That's not completely true. I forked @kunicmarko20's project and replaced annotations with attributes: https://github.com/nucleos/SonataAutoConfigureBundle

Hanmac commented 2 years ago

Annotation are deprecated/archived.

That's not completely true. I forked @kunicmarko20's project and replaced annotations with attributes: https://github.com/nucleos/SonataAutoConfigureBundle

does your fork has the ability to order the Sections in the Sonata Admin Menu, or would need that more changes for this bundle?

core23 commented 2 years ago

does your fork has the ability to order the Sections in the Sonata Admin Menu, or would need that more changes for this bundle?

That's not possible at the moment, because the original project does not provide this feature and I don't depend on the order.

Feed free to port this feature to the sonata bundle or provide a priority feature.

eerison commented 2 years ago

well I guess this issue could be closed?!

right @tdumalin ?

I created this ticket to add the tip in the docs: #7820

tdumalin commented 2 years ago

@eerison , yes this issue is related to the PR. Great news for the docs, hope it will be useful !