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

Working example of auto-discovery through composer plugin #46

Closed
pmall opened this issue Mar 5, 2018 · 3 comments
Closed

Working example of auto-discovery through composer plugin #46

pmall opened this issue Mar 5, 2018 · 3 comments

Comments

@pmall
Copy link

pmall commented Mar 5, 2018

For my personal project I came up with an approach using a composer installer plugin for service providers auto-discovery.

I'd like to show you a working example and I'm very eager to hear about your remarks and criticisms before going further with it.

Approach overview

Have a provider installer plugin for service-provider package types. Those packages contain a providers.php file at root, returning an array of service providers they expose:

# file located at: {vendor}/{name}/providers.php
<?php

return [
    new Vendor\Name\SomePackageServiceProvider1,
    new Vendor\Name\SomePackageServiceProvider2,
    ...
];

Then a project can define a destination folder path under an extra key of its composer.json file.

  • When this key is absent nothing happen and service-provider packages are installed like any other packages.
  • When this key is present, service-provider package's providers.php files are imported in the defined destination folder on install, following a naming scheme ensuring no collision.

providers.php files are imported only on the service-provider package first install and are removed when uninstalled.

It is up to the project developer to collect imported service provider, for example by using a simple glob pattern.

Example

Lets install the example project:

git clone git@github.com:pmall/providers-example.git

In the composer.json file you can see a few required service-provider packages:

Two from my personal project for the sake of example:

  • ellipse/providers-http
  • ellipse/providers-session

Another one under the services vendor namespace for a more generic service:

  • services/dbal

The extra['providers']['path'] key of the composer.json file is set to the project config directory, which is empty when creating the project.

After running composer install you can see the packages providers.php files has been imported in the config directory:

providers-example/config/ellipse/dispatcher.php
providers-example/config/ellipse/http.php
providers-example/config/ellipse/session.php
providers-example/config/services/dbal.php

They are grouped by vendor namespace so paths can't collide. Plus 'providers-' prefixes and '-providers' suffixes are removed from the package names. It allows a vendor namespace to both contain a package named {vendor}/{name} for the actual package and a service-provider package named {vendor}/providers-{name} for its service providers.

You probably noticed ellipse/dispatcher.php file has been imported whereas ellipse/providers-dispatcher it is not a dependency of the root composer.json file. It has been imported because it is a dependency of ellipse/providers-http package.

Also (interesting to mention even if what the imported service provider does is irrelevant for this presentation) those service provider constructors take an array of factory extensions, they are exposing and documenting values the developer would configure.

Finally the container.php file at the root of the project directory collect them and build a working Psr-11 container with a simple glob pattern. You can require it in a php cli and call get() with a provided id.

Detailed mechanism

  • Import is performed when:
    • A destination folder path is defined in the project's composer.json file.
    • This is the first time the package is installed, meaning no composer.lock file is present or the package name is not present in the composer.lock file. It ensures an imported providers.php file which was manually renamed/deleted during development is not imported again when the application is deployed.
    • A providers.php file is present at the service-providers package root.
    • A {destination path}/{vendor}/{name}.php file is not already present in the project.
  • The path of the destination file is {destination path}/{vendor}/{name}.php + remove 'providers-' prefixes and '-providers' suffixes from the package names.
  • The {destination path}/{vendor}/{name}.php file is removed if present when the package is uninstaled.

Why I like this approach

  • Auto discovery is totally optional. Developers have to explicitly choose they want providers.php files to be imported in their project by defining a destination folder path. Yet service-provider packages are still useful without auto-dicovery enabled because service providers can still be consumed manually.
  • The installer plugin is very lightview. It calls the default composer install and uninstall methods then perform a few checks before copying/deleting the providers.php file in the destination folder path.
  • No special syntax is needed as it uses only plain php. Developers can freely edit the imported providers.php files, like comment service providers out, change their order or completely delete some. Service providers can even come with constructor parameters with suitable default values.
  • Content of the imported providers.php files is versioned under the project VCS.
  • It works with any service provider implementation.
  • Developper can freely choose how imported service providers are collected. Simple glob patterns should be enough in most cases but more complex logic can be performed depending on the project.
  • service-provider packages can require another one.
  • service-provider packages can be set as dev requirements in which case the imported providers.php file path would reflect this. Not sure about what convention would be the best but it is achievable. Developers could then have a collecting logic importing dev service providers only in dev environment. For example, try to remove services/dbal from the example above and install it again as dev requirement.
  • Nothing happen on package update. It should not be a problem because updating a package to a version with breaking changes should not happen automatically. This approach provides a nice boilerplate configuration on the first install, then developers should be aware of what they do when they update to a version with breaking changes.
  • They are grouped by vendor namespaces ensuring there is no collision and files are nicely organized in the destination folder. Complex libraries can require many service-provider packages and their providers.php files are grouped together under the same vendor namespace.
  • The installer plugin package can be maintained by the community.
  • A special vendor namespace (like services) can be maintained by the community and contain service-providers packages for widely used php package.

So what's your through about this ?

@moufmouf
Copy link
Contributor

moufmouf commented Mar 6, 2018

Hey Pierre!

First, I'm really glad you open the topic of auto-discovery.

Your idea looks a bit like what Flex is doing in Symfony 4 (copying configuration files in the user workspace where the user can freely change those).

I have been playing a bit myself with autodiscovery and went a completely different way.
If you haven't heard of it yet, you can check thecodingmachine/discovery.
I used Puli as an inspiration. It does not copy files around. Instead, it relies on configuration files being "merged" at install/update time. Flexibility is kept: you can cancel the registration of any service provider using a CLI tool.

Regarding your proposal, I like it a lot too. There is only one issue I see. If you require service providers using a glob, you are not controlling the order of loading of your service providers. Yet, order sometimes matter (especially when a service provider is overriding the services of another service provider). You would need to find a way to control the order of loading of the service providers.

If we look at the broad picture, a lot is done regarding auto-discovery nowaday. Symfony 4 has Flex (which really is an autodiscovery of Symfony bundles + some additional tools). Laravel 5.5 introduced auto-discovery too. Zend Expressive has it.

I would really be thrilled if we could find a common way for packages to expose that they are providing service providers.

It could be done using a "providers.php" file like you did. It could be a JSON file. Or we could put it in the "extra" section of the composer.json file... I don't think we need to go much further. On top of that, any framework could provide the autodiscovery mechanism that best fits its needs.

What do you think?

@pmall
Copy link
Author

pmall commented Mar 6, 2018

Hi David thanks for your answer,

Puli seemed a bit ambitious to me (if I remember well it attempts to manage both type of assets + assets). Also the project seems dead. Concerning your project, I saw it a while ago. If I understand well the published assets ultimately look like arrays provided by service providers?

There is only one issue I see. If you require service providers using a glob, you are not controlling the order of loading of your service providers.

First just to be clear this approach does not require to use a glob pattern: it is just focusing on how to properly import providers.php files, the collecting logic is left to the project/framework.

Yet, order sometimes matter (especially when a service provider is overriding the services of another service provider)

About the order the service providers are imported, my opinion is well designed service providers of packages should not depend on the order they are loaded.

I can't figure out a case where a package should have priority over another ? Could you show me an example ? If you are thinking about multiple implementations of the same interface, we agree we should have a way to handle that.

On the other side, yes the project service providers should be loaded after auto-discovered ones if they override some of their entries. That's why collecting logic is left to the project/framework. You can collect all the imported packages service providers then load the project ones in the order needed.

It could be done using a "providers.php" file like you did. It could be a JSON file. Or we could put it in the "extra" section of the composer.json file...

Going with php files is nice because once imported you can edit them like any other php files. I remember conversations here about commenting out a service provider to track bugs, or even use service providers with constructor parameters, well this is simple and easy with php files and totally impossible with json.

On top of that, any framework could provide the autodiscovery mechanism that best fits its needs.

Yes it is also possible to let frameworks have their own mechanism for importing providers.php files.

I fact my first approach was a regular composer plugin any project could use to import providers.php files + the destination folder path in the extra keys.

Then I went with a composer installer plugin because I felt it was more elegant to just have to configure the destination path.

But you're right, frameworks could choose to adopt another naming convention for imported files or whatever.

I don't really care which way to go. I just think there should be a generic composer plugin for non-framework users, maintained by multiple people. I think there is more and more developers embracing a non-framework philosophy (including myself) and there should be a standard solution for them.

Also I think it would be cool to have one vendor namespace with generic service providers packages for many popular php packages. A bit like Oscar's middleware repo.

@mindplay-dk
Copy link
Collaborator

Since this relates to the topic of service provider discovery, I am closing this issue - the service provider PSR itself does not require service discovery.

I understand that service discovery is something that several members of this working group desired, but a service discovery standard (which might depend on this PSR) could be defined independently.

I am closing this issue in order to reduce scope.

If you think further discussion is require, please open a thread in Discussions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants