Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ServiceManager inheritance for AbstractPluginManager #179

Merged
merged 17 commits into from
May 2, 2023

Conversation

boesing
Copy link
Member

@boesing boesing commented Feb 23, 2023

Q A
Documentation no
BC Break yes

Description

This is a PoC of how the plugin manager could work without inheriting from the ServiceManager.
With this PoC, I've also modified ServiceManager to return mixed as thats actually the case already. Nothing in this component verifies that something is object. We do have soft-types for factories to only return object but in fact, they can return mixed anyways.
Since the AbstractPluginManager does not require an $instanceOf value and thus, can also return mixed in case it is not set, I think this change makes sense until we decide to go the same route as symfony does. Since that decision has not yet been made, I'd prefer to skip that. I'm open to create an RFC for this and bring it to the next TSC if some1 wants to further discuss this.

Please NOTE that I also:

  • removed some useless tests which verified exceptions for passing invalid value sto either ServiceManager and/or AbstractPluginManager
  • added plenty of type-hints (even to interfaces) - but not to every interface (yet) - will definitely do that before finally creating a v4 (@samsonasik would you mind updating the laminas/laminas-servicemanager-migration rector rules once we finished with adding all the missing types?)
  • slightly modified the ServiceManagerConfigurationType (still compatible but more strict)
  • with a psalm version containing this patch, we can provide even better psalm-types regarding callable-objects and class-string

TODOS:

  • Modify container-config-test repository so that some tests are being dropped
  • Mark ConfigInterface and Config as deprecated in v3
  • Add psalm-types from ConfigInterface to ServiceManager in a redundant way (in v3) so that upstream projects can already switch
  • Mark ServiceManager as @final in v3
  • Get Introduce callable object intersection vimeo/psalm#9599 merged so that we can provide even better psalm-types for class-string<callable(ContainerInterface,string,array|null):mixed>

@boesing boesing added this to the 4.0.0 milestone Feb 23, 2023
* @group mutation
* @covers \Laminas\ServiceManager\ServiceManager::mapLazyService
*/
public function testCanMapLazyServices(): void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this test to the ServiceManager specific tests as this is used for both AbstractPluginManager AND ServiceManager.

I could've checked the container instance and marked as skipped but since there is already a dedicated test class, I moved it.

@boesing boesing force-pushed the feature/plugin-manager-composition branch from bc629f5 to c11f339 Compare February 23, 2023 20:16
@samsonasik
Copy link
Member

@boesing ok , I can do that 👍 , I may need summary which part that need update

@boesing
Copy link
Member Author

boesing commented Mar 5, 2023

Partly solves #44

Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only major problem with this patch is the addition of #configure() on interfaces that are designed for container consumption, rather than configuration.

Configuration of a container can happen on concrete instances, while the ContainerInterface, ServiceLocatorInterface and PluginManagerInterface interfaces should be read-only, without any assumption that they might be configurable.

Heck, they may be dumped/generated code.

psalm.xml.dist Show resolved Hide resolved
src/AbstractFactory/ReflectionBasedAbstractFactory.php Outdated Show resolved Hide resolved
src/AbstractPluginManager.php Show resolved Hide resolved
src/Config.php Outdated Show resolved Hide resolved
src/Config.php Outdated Show resolved Hide resolved
src/PluginManagerInterface.php Outdated Show resolved Hide resolved
src/ServiceLocatorInterface.php Outdated Show resolved Hide resolved
src/ServiceManager.php Outdated Show resolved Hide resolved
src/ServiceManager.php Show resolved Hide resolved
src/ServiceManager.php Outdated Show resolved Hide resolved
@Xerkus
Copy link
Member

Xerkus commented Mar 7, 2023

configure() is/was part of mutable container. I remember there was a push to make container immutable and configurable at creation. It got ultimately dropped unmerged or reverted before release.

Its use case might have been related to how container or various plugin managers were configured after creation in module manager/mvc events.

I am for making it internal implementation detail and removing from public interface.

@Xerkus
Copy link
Member

Xerkus commented Mar 7, 2023

configure() is indeed mvc late configuration related. Context could be found here zendframework/zend-servicemanager#47

@boesing
Copy link
Member Author

boesing commented Mar 7, 2023

@Xerkus @Ocramius I'd be up for a read-only container with v5. Given the fact that v4 to the end of Q1 is already tough with all the migration paths, I'd prefer not being too complex here.

Keeping the old methods is mostly due to BC compat but I am fine with dropping configure from the Interface. WDYT?

@Ocramius
Copy link
Member

Ocramius commented Mar 7, 2023

Dropping from the interface(s) is sufficient for me, for now 👍

@boesing boesing force-pushed the feature/plugin-manager-composition branch from 6548c34 to e9c797a Compare March 9, 2023 12:58
@boesing boesing marked this pull request as draft April 6, 2023 02:51
@boesing boesing force-pushed the feature/plugin-manager-composition branch from e9c797a to f445aa9 Compare April 6, 2023 02:52
@boesing boesing changed the title [PoC] Remove ServiceManager inheritance for AbstractPluginManager Remove ServiceManager inheritance for AbstractPluginManager Apr 8, 2023
@boesing boesing force-pushed the feature/plugin-manager-composition branch from f445aa9 to 8f92678 Compare April 20, 2023 00:51
…e plugins

This removes the inheritance of the `ServiceManager` and marks the `ServiceManager` `final` in a soft state.

Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing boesing force-pushed the feature/plugin-manager-composition branch from 8f92678 to 4845306 Compare April 20, 2023 00:53
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing

This comment was marked as outdated.

Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing boesing force-pushed the feature/plugin-manager-composition branch from 381e588 to 827be21 Compare April 26, 2023 18:06
@boesing boesing marked this pull request as ready for review April 26, 2023 18:08
composer.json Show resolved Hide resolved
composer.json Outdated
"phpbench/phpbench": "^1.2.7",
"phpunit/phpunit": "^9.5.26",
"psalm/plugin-phpunit": "^0.18.0",
"symfony/console": "^6.0",
"vimeo/psalm": "^5.0.0"
"vimeo/psalm": "5.x-dev"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • needs to be switched back once psalm releases the next minor

@boesing boesing linked an issue Apr 26, 2023 that may be closed by this pull request
Copy link
Member

@weierophinney weierophinney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall,I like this approach. Composing a ServiceManager inside the AbstractPluginManager removes a lot of duplication, while simultaneously removing the inheritance. Having an interface means we can use that as the type instead of an abstract class, which also makes me happy. And the psalm shapes in the ServiceManager provide valuable documentation of expectations.

My only concern/issue is that I don't recall the the rationale for deprecating/removing the ConfigInterface and Config class. I recognize that we can use Psalm annotations to provide shapes for valid data, but that seems more likely to lead to issues at runtime where the shapes cannot be verified/validated. That said, I won't block approval on that; just wish I could remember what the arguments were around this.

Comment on lines 29 to 41
public function has(string $id): bool;

/**
* @template TRequestedInstance extends InstanceType
* @psalm-param class-string<TRequestedInstance>|string $id Service name of plugin to retrieve.
* @psalm-return ($id is class-string ? TRequestedInstance : InstanceType)
* @throws Exception\ServiceNotFoundException If the manager does not have
* a service definition for the instance, and the service is not
* auto-invokable.
* @throws InvalidServiceException If the plugin created is invalid for the
* plugin context.
*/
public function get(string $id): mixed;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since ServiceLocatorInterface already extends ContainerInterface, should these really be defined here?

I can understand defining get() so we can add the additional annotations, but at the very least, has() feels superfluous.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yah, has is superfluous indeed.
I guess I've added it to also add some psalm-assert on it to provide information of what will be returned from get. But since has actually does no validation at all, it would provide false information and thus we can safely drop it here.

@boesing
Copy link
Member Author

boesing commented Apr 27, 2023

 [...] rationale for deprecating/removing the ConfigInterface and Config [...]

I don't see any value for that interface or the Config. The interface itself was not used anywhere and the Config was almost always empty.
We (in my company) configured our project container like this in mezzio:

container.php

<?php

use Laminas\ServiceManager\Config;
use Laminas\ServiceManager\ServiceManager;

require_once __DIR__ . '/../vendor/autoload.php';

// Load configuration
$config = require __DIR__ . '/config.php';

// Build container
$container = new ServiceManager();
(new Config($config['dependencies']))->configureServiceManager($container);

// Inject config
$container->setService('Config', $config);

// Make container writable since we want to overwrite even existing alias for config
$container->setAllowOverride(true);
$container->setAlias('config', 'Config');
$container->setAllowOverride(false);

return $container;

The laminas-modulemanager seems to provide support for the ConfigInterface and maybe some projects out there might have used that. I guess that was to avoid having to provide the array-structure from Module#getConfig. But since that is the usual way of how it is returned from ConfigProvider#__invoke in mezzio applications, the plan for laminas-mvc v4 is to use the config-aggregator for modules as well (hopefully, that would be huge, can be even done as of now by using laminas/laminas-config-aggregator-modulemanager) and due to the fact that we have psalm-types for a period of time now gives me the feeling that the ConfigInterface along with its implementation is adding unnecessary code-paths to applications (as in the end, we end up having an array anyways which is passed to ServiceManager#configure or AbstractPluginManager#configure.

So its just a dangling class mostly used to have a value-object of the config which wasnt properly validated at runtime and didn't provide any type until 3.9.0 which was 11 years after the initial creation of the interface.


Thats why I think we can safely drop it and let upstream projects actually return the array structure while using psalm + psalm-type to have proper type validation:

before

<?php

namespace MyModule;

use Laminas\ModuleManager\Feature\ServiceProviderInterface;
use Laminas\ServiceManager\Config;
use Laminas\ServiceManager\Factory\InvokableFactory;

final class Module implements ServiceProviderInterface
{

    public function getServiceConfig(): Config
    {
        return new Config([
            'factories' => [
                MyModuleService::class => InvokableFactory::class,
            ],
        ]);
    }
}

after

<?php

namespace MyModule;

use Laminas\ModuleManager\Feature\ServiceProviderInterface;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Laminas\ServiceManager\ServiceManager;

/** @psalm-import-type ServiceManagerConfiguration from ServiceManager */
final class Module implements ServiceProviderInterface
{

    /**
     * @return ServiceManagerConfiguration
     */
    public function getServiceConfig(): array
    {
        return [
            'factories' => [
                MyModuleService::class => InvokableFactory::class,
            ],
        ];
    }
}

However, I am open to keep the Config object if we prefer object > array but then I would actually prefer the config being injected as an object to the SM/PluginManager instead of allowing arrays. But then we have ConfigInterface#toArray again which does not really change anything.

Should we create an RFC for this? I can restore the ConfigInterface + Config here for now.
But we would have to rename ConfigInterface#configureServiceManager to ConfigInterface#configure and then allow both ServiceManager + AbstractPluginManager due to the removal of inheritance here.

@weierophinney
Copy link
Member

@boesing Thanks for that summary!

When I consider that the majority of the time, we're passing an array to either the ServiceManager or a plugin manager, and that it has to do validations internally anyways due to this, it doesn't make sense to keep it. The only reason to keep it is if the SM and/or PM required an instance, and pulled configuration from it, as it would then provide validations. That would just be shifting responsibilities, and would introduce duplications regardless, as we have the various set*() methods, and they have to do validations, too.

Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing
Copy link
Member Author

boesing commented Apr 30, 2023

@boesing ok , I can do that 👍 , I may need summary which part that need update

@samsonasik I guess I've finished the work here, so I can provide a lil summary:

  1. all classes extending AbstractPluginManager should either
    1.1 extend AbstractSingleInstancePluginManager in case the $instanceOf property is declared and should adapt $instanceOf value in @template-extends annotation
    1.2 implement its own validate method (can be created as an empty method from rector in case it is missing)
  2. We could search for classes implementing the modulemanager ServiceProviderInterface#getServiceConfig and in case of a return value of Config it should return array (basically the __construct($arg) which is being passed to the Config. Refer to Remove ServiceManager inheritance for AbstractPluginManager #179 (comment) to see how it needs to be replaced.
  3. adding property types of AbstractPluginManager/ AbstractSingleInstancePluginManager (not sure if rector can modify code which is not valid as per linting due to mismatching property types?)
  4. container.php as in Remove ServiceManager inheritance for AbstractPluginManager #179 (comment) can be modified to $container = new ServiceManager($config['dependencies']); instead

Not 100% sure if that is already everything but I guess at least for this PR that should reflect almost all changes.

@samsonasik
Copy link
Member

@boesing Thank you for the summary, I will try what I can do 👍

Copy link
Member

@weierophinney weierophinney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

@boesing boesing force-pushed the feature/plugin-manager-composition branch from 1ee6810 to d38a600 Compare May 2, 2023 17:36
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing boesing force-pushed the feature/plugin-manager-composition branch from d38a600 to 8f14d75 Compare May 2, 2023 17:44
Signed-off-by: Maximilian Bösing <2189546+boesing@users.noreply.github.com>
@boesing boesing merged commit 3d870a0 into laminas:4.0.x May 2, 2023
12 checks passed
@boesing boesing deleted the feature/plugin-manager-composition branch May 2, 2023 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment