pkp / pkp-lib

The library used by PKP's applications OJS, OMP and OPS, open source software for scholarly publishing.
https://pkp.sfu.ca
GNU General Public License v3.0
304 stars 444 forks source link

Hook for service registration in a plugin #4383

Closed ViRuSTriNiTy closed 4 years ago

ViRuSTriNiTy commented 5 years ago

Hi there,

as described here ...

https://forum.pkp.sfu.ca/t/hook-for-service-registration-in-a-plugin/50799

... there should be a hook for providing services from a plugin.

I will provide a PR.

So lonG

NateWr commented 5 years ago

This is a great idea, thanks @ViRuSTriNiTy!

I'm trying to think through the implications here, and figure out what is the best approach. One concern I have with putting it into Services::get() is that it's called over and over again. I understand why it can't be in the init(), but I also worry that if a plugin can inject a service at any time, we'll end up with a lot of headaches down the line where a developer won't be able to know when a service will be registered. They'll have to find it out on a plugin-by-plugin basis.

In WordPress, there are a few low-level hooks like init, admin_init, after_plugins_loaded and after_themes_loaded. These are good for when a plugin or theme wants to register some kind of asset that is available generally.

I'd like to try a similar approach here. On the one hand, we'll have a bit more certainty about when a custom Service is registered. On the other hand, we'll be able to use the same hook for lots of things like this, which will help keep our documentation smaller (when we get to writing it out properly!).

Here's what I'm thinking.

Every request to the application is kicked off by Dispatcher::dispatch(). Here, every generic plugin is loaded and the context schema is reinitialized:

https://github.com/pkp/pkp-lib/blob/master/classes/core/Dispatcher.inc.php#L131-L138

AppLocale::initialize($request);
PluginRegistry::loadCategory('generic', true);

// Reload the context after generic plugins have loaded so that changes to
// the context schema can take place
import('classes.core.Services');
$contextSchema = \Services::get('schema')->get(SCHEMA_CONTEXT, true);
$request->getRouter()->getContext($request, 1, true);

I'd like to add a new hook here:

AppLocale::initialize($request);
PluginRegistry::loadCategory('generic', true);

HookRegistry::call('Dispatcher::dispatch', $request);

// Reload the context after generic plugins have loaded so that changes to
// the context schema can take place
import('classes.core.Services');
$contextSchema = \Services::get('schema')->get(SCHEMA_CONTEXT, true);
$request->getRouter()->getContext($request, 1, true);

Then, in PKPServices, we should add a static method for registering a new ServiceContainer:

public static function register($service) {
    self::_instance()->container->register($service);
}

If a plugin wants to register a new Service, it should then create a new ServiceProvider class, just like the OJSServiceProvider:

namespace PluginName;
use \Pimple\Container;
class PluginNameServiceProvider implements \Pimple\ServiceProviderInterface {
    public function register(Container $pimple) {
        $pimple['serviceName'] = function() {
            return new \PluginName\ServiceNameService();
        };
    }
}

Finally, it should hook to Dispatcher::dispatch and register the new container:

public function registerServiceContainer($hookName, $request) {
    Services::register(new \PluginName\PluginNameServiceProvider);
}

Cons:

Pros:

Notes:

@asmecher do you have any thoughts about this?

@ViRuSTriNiTy I haven't tested this at all, but would you be willing to work up a Pull Request along these lines, once we have input from Alec?

ViRuSTriNiTy commented 5 years ago

@NateWr Sounds reasonable, waiting for response from @asmecher

asmecher commented 5 years ago

Hi all -- sorry for the delayed response. @NateWr's proposal seems well considered and it looks like you're in agreement, @ViRuSTriNiTy.

(Potentially getting off on a tangent here -- but in rewriting the tests for a newer PHPUnit I had to introduce some changes to reduce the dependence on static function calls e.g. by using singleton or factory patterns instead. I see you're using _instance here. Can we standardize on a function name for this? We have e.g. TemplateManager::getManager, various Application/PKPApplication factory methods, and the soon-to-be-merged Application::get. I think instance (or _instance where it's protected/private) are the clearest.)

NateWr commented 5 years ago

Just pointing out that the use of _instance is part of the Pimple dependency injection library that Kassim integrated to manage our services.

It's pretty small so would be easy to replace with something else if we want. We had a PHP 5.3 compatibility requirements at the time so may be able to find something more modern now.

ViRuSTriNiTy commented 4 years ago

@asmecher Today i came back to this issue as i wanted to use a service from plugin B in plugin A. Should i provide a PR with the proposed changes by @NateWr?

NateWr commented 4 years ago

Yes, thanks @ViRuSTriNiTy!

ViRuSTriNiTy commented 4 years ago

@NateWr I have added some commits to incorporate your suggestions from https://github.com/pkp/pkp-lib/issues/4383#issuecomment-458078527 and tested it in my customized OJS, works really nicely and the code for service registration in plugins looks much cleaner now. 👍

Would you please complete the PR by reviewing it? Thanks.

Btw: The rename of ServicesContainer to Services is not included in the current release of OJS. I had to rework some files to get it working. When will this change be included?

NateWr commented 4 years ago

Thanks @ViRuSTriNiTy! We're having some hiccups with our tests at the moment so I've gone ahead and merged without them. It looks pretty clean and hard to imagine how it would break the tests anyway.

When will this change be included?

That's part of changes for 3.2, which is scheduled for release by the end of February. We'll provide a more thorough account of the changes related to this release in a blog post or a page in the docs hub. In the meantime there's a short-hand list here, unfortunately with very little detail: https://github.com/pkp/pkp-docs/issues/235